From 70c992f005fd7e23580e50149333a7b938d350a3 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 3 Aug 2025 13:10:12 +1200 Subject: [PATCH 01/24] Changing WebUI to include display device name, and change it to select drop down rather than checkbox --- main/TheengsCommon.h | 1 + main/config_WebContent.h | 2 +- main/config_WebUI.h | 4 ++++ main/config_mqttDiscovery.h | 4 ---- main/gatewayBT.cpp | 15 ++++++++++----- main/mqttDiscovery.cpp | 2 +- main/webUI.cpp | 23 +++++++++++++++++++---- 7 files changed, 36 insertions(+), 15 deletions(-) diff --git a/main/TheengsCommon.h b/main/TheengsCommon.h index 17a0fe8469..73c9930d6c 100644 --- a/main/TheengsCommon.h +++ b/main/TheengsCommon.h @@ -62,6 +62,7 @@ extern bool ready_to_sleep; extern char mqtt_topic[]; extern char gateway_name[]; extern unsigned long lastDiscovery; // Time of the last discovery to trigger automaticaly to off after DiscoveryAutoOffTimer +extern bool displayDeviceName; extern bool enqueueJsonObject(const StaticJsonDocument& jsonDoc, int timeout); extern bool enqueueJsonObject(const StaticJsonDocument& jsonDoc); diff --git a/main/config_WebContent.h b/main/config_WebContent.h index 9215d0d9e7..4544d3f394 100644 --- a/main/config_WebContent.h +++ b/main/config_WebContent.h @@ -112,7 +112,7 @@ const char config_gateway_body[] = body_header "
OpenMQTTGateway Logging

Log Level


" body_footer_config_menu; -const char config_webui_body[] = body_header "
Configure WebUI

Display Metric

Secure WebUI


" body_footer_config_menu; +const char config_webui_body[] = body_header "
Configure WebUI

Display temperature

Device naming

Secure WebUI


" body_footer_config_menu; const char config_rf_body[] = body_header "
" diff --git a/main/config_WebUI.h b/main/config_WebUI.h index 5ba02a4261..5f1cef6edc 100644 --- a/main/config_WebUI.h +++ b/main/config_WebUI.h @@ -42,6 +42,10 @@ # define DISPLAY_METRIC true // Units used for display of sensor data #endif +#ifndef DISPLAY_DEVICE_NAME +# define DISPLAY_DEVICE_NAME false // Set to true to force the device name to be from the name of the device and not the model +#endif + #ifndef DISPLAY_WEBUI_INTERVAL # define DISPLAY_WEBUI_INTERVAL 3 // Number of seconds between json message displays #endif diff --git a/main/config_mqttDiscovery.h b/main/config_mqttDiscovery.h index 7cb89e22cd..20d4ee8e95 100644 --- a/main/config_mqttDiscovery.h +++ b/main/config_mqttDiscovery.h @@ -127,10 +127,6 @@ extern char discovery_prefix[]; # define GATEWAY_MANUFACTURER "OMG_community" #endif -#ifndef ForceDeviceName -# define ForceDeviceName false // Set to true to force the device name to be from the name of the device and not the model -#endif - /*-------------- Auto discovery macros-----------------*/ // Home assistant autodiscovery value key definition #define jsonBatt "{{ value_json.batt | is_defined }}" diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index b3cd3328ad..dfa05c8807 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -951,11 +951,11 @@ void launchBTDiscovery(bool overrideDiscovery) { Log.trace(F("properties: %s" CR), properties.c_str()); std::string brand = decoder.getTheengAttribute(p->sensorModel_id, "brand"); std::string model = decoder.getTheengAttribute(p->sensorModel_id, "model"); -# if ForceDeviceName - if (p->name[0] != '\0') { - model = p->name; + if (displayDeviceName) { + if (p->name[0] != '\0') { + model = p->name; + } } -# endif std::string model_id = decoder.getTheengAttribute(p->sensorModel_id, "model_id"); // Check for tracker status @@ -1010,7 +1010,12 @@ void launchBTDiscovery(bool overrideDiscovery) { Log.trace(F("Key: %s"), prop.key().c_str()); Log.trace(F("Unit: %s"), prop.value()["unit"].as()); Log.trace(F("Name: %s"), prop.value()["name"].as()); - String entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); + String entity_name = ""; + if (displayDeviceName) { + entity_name = String(model.c_str()) + "-" + String(prop.key().c_str()); + } else { + entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); + } String unique_id = macWOdots + "-" + String(prop.key().c_str()); String value_template = "{{ value_json." + String(prop.key().c_str()) + " | is_defined }}"; if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1 && strcmp(prop.key().c_str(), "state") == 0) { diff --git a/main/mqttDiscovery.cpp b/main/mqttDiscovery.cpp index 2f064cf3a9..343ea5dace 100644 --- a/main/mqttDiscovery.cpp +++ b/main/mqttDiscovery.cpp @@ -586,7 +586,7 @@ void createDiscovery(const char* sensor_type, // generate unique device name by adding the second half of the device_id only if device_name and device_id are different and we don't want to use the BLE name if (device_name[0]) { - if (strcmp(device_id, device_name) != 0 && device_id[0] && !ForceDeviceName) { + if (strcmp(device_id, device_name) != 0 && device_id[0] && !displayDeviceName) { device["name"] = device_name + String("-") + String(device_id + 6); } else { device["name"] = device_name; diff --git a/main/webUI.cpp b/main/webUI.cpp index ea033876d6..bbbfa3415d 100644 --- a/main/webUI.cpp +++ b/main/webUI.cpp @@ -85,6 +85,7 @@ const char* www_username = WEBUI_LOGIN; String authFailResponse = "Authentication Failed"; bool webUISecure = WEBUI_AUTH; boolean displayMetric = DISPLAY_METRIC; +boolean displayDeviceName = DISPLAY_DEVICE_NAME; /*********************************************************************************************\ * ESP32 AutoMutex @@ -502,7 +503,8 @@ void handleCN() { /** * @brief /WU - Configuration Page * T: handleWU: uri: /wu, args: 3, method: 1 - * T: handleWU Arg: 0, dm=on - displayMetric + * T: handleWU Arg: 0, dm=1 - displayMetric + * T: handleWU Arg: 0, dn=1 - displayDeviceName * T: handleWU Arg: 1, sw=on - webUISecure * T: handleWU Arg: 2, save= */ @@ -515,10 +517,19 @@ void handleWU() { } bool update = false; - if (displayMetric != server.hasArg("dm")) { + if (server.hasArg("dm") && server.arg("dm").toInt() != displayMetric) { + WEBUI_TRACE_LOG(F("handleWU Update displayMetric from: %d" CR), displayMetric); + displayMetric = server.arg("dm").toInt(); + WEBUI_TRACE_LOG(F("handleWU Update displayMetric to: %d" CR), displayMetric); + update = true; + } + + if (server.hasArg("dn") && server.arg("dn").toInt() != displayDeviceName) { + WEBUI_TRACE_LOG(F("handleWU Update displayDeviceName from: %d" CR), displayDeviceName); + displayDeviceName = server.arg("dn").toInt(); + WEBUI_TRACE_LOG(F("handleWU Update displayDeviceName from: %d" CR), displayDeviceName); update = true; } - displayMetric = server.hasArg("dm"); if (webUISecure != server.hasArg("sw")) { update = true; @@ -540,7 +551,7 @@ void handleWU() { response += String(script); response += String(style); int logLevel = Log.getLevel(); - snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_webui_body, jsonChar, gateway_name, (displayMetric ? "checked" : ""), (webUISecure ? "checked" : "")); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_webui_body, jsonChar, gateway_name, (displayMetric ? "selected" : ""), (!displayMetric ? "selected" : ""), (!displayDeviceName ? "selected" : ""), (displayDeviceName ? "selected" : ""), (webUISecure ? "checked" : "")); response += String(buffer); snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION); response += String(buffer); @@ -1770,6 +1781,7 @@ String stateWebUIStatus() { StaticJsonDocument WebUIdataBuffer; JsonObject WebUIdata = WebUIdataBuffer.to(); WebUIdata["displayMetric"] = (bool)displayMetric; + WebUIdata["displayDeviceName"] = (bool)displayDeviceName; WebUIdata["webUISecure"] = (bool)webUISecure; WebUIdata["displayQueue"] = uxQueueMessagesWaiting(webUIQueue); @@ -1786,6 +1798,7 @@ bool WebUIConfig_save() { StaticJsonDocument jsonBuffer; JsonObject jo = jsonBuffer.to(); jo["displayMetric"] = (bool)displayMetric; + jo["displayDeviceName"] = (bool)displayDeviceName; jo["webUISecure"] = (bool)webUISecure; // Save config into NVS (non-volatile storage) String conf = ""; @@ -1799,6 +1812,7 @@ bool WebUIConfig_save() { void WebUIConfig_init() { displayMetric = DISPLAY_METRIC; + displayDeviceName = DISPLAY_DEVICE_NAME; webUISecure = WEBUI_AUTH; Log.notice(F("WebUI config initialised" CR)); } @@ -1819,6 +1833,7 @@ bool WebUIConfig_load() { } JsonObject jo = jsonBuffer.as(); displayMetric = jo["displayMetric"].as(); + displayDeviceName = jo["displayDeviceName"].as(); webUISecure = jo["webUISecure"].as(); return true; } else { From 5f20c72f0e60411692ccba08d3c33b2c7494b330 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 3 Aug 2025 14:17:28 +1200 Subject: [PATCH 02/24] Fix mqttDiscovery to require WebUI and ESP32 for displayDeviceName --- main/mqttDiscovery.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/main/mqttDiscovery.cpp b/main/mqttDiscovery.cpp index 343ea5dace..4c7b52ef13 100644 --- a/main/mqttDiscovery.cpp +++ b/main/mqttDiscovery.cpp @@ -586,7 +586,11 @@ void createDiscovery(const char* sensor_type, // generate unique device name by adding the second half of the device_id only if device_name and device_id are different and we don't want to use the BLE name if (device_name[0]) { + #if defined(ZwebUI) && defined(ESP32) // displayDeviceName only applies when running with the WebUI and ESP32 if (strcmp(device_id, device_name) != 0 && device_id[0] && !displayDeviceName) { + #else + if (strcmp(device_id, device_name) != 0 && device_id[0]) { + #endif device["name"] = device_name + String("-") + String(device_id + 6); } else { device["name"] = device_name; From df1d711245745ab52031739987b10fa49cbcc137 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 3 Aug 2025 14:27:36 +1200 Subject: [PATCH 03/24] Fix mqttDiscovery to require WebUI and ESP32 and ESP8266 for displayDeviceName and ForceDeviceName --- main/config_mqttDiscovery.h | 4 ++++ main/gatewayBT.cpp | 4 ++-- main/mqttDiscovery.cpp | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/main/config_mqttDiscovery.h b/main/config_mqttDiscovery.h index 20d4ee8e95..7cb89e22cd 100644 --- a/main/config_mqttDiscovery.h +++ b/main/config_mqttDiscovery.h @@ -127,6 +127,10 @@ extern char discovery_prefix[]; # define GATEWAY_MANUFACTURER "OMG_community" #endif +#ifndef ForceDeviceName +# define ForceDeviceName false // Set to true to force the device name to be from the name of the device and not the model +#endif + /*-------------- Auto discovery macros-----------------*/ // Home assistant autodiscovery value key definition #define jsonBatt "{{ value_json.batt | is_defined }}" diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index dfa05c8807..fbe5958fa8 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -951,7 +951,7 @@ void launchBTDiscovery(bool overrideDiscovery) { Log.trace(F("properties: %s" CR), properties.c_str()); std::string brand = decoder.getTheengAttribute(p->sensorModel_id, "brand"); std::string model = decoder.getTheengAttribute(p->sensorModel_id, "model"); - if (displayDeviceName) { + if (displayDeviceName || ForceDeviceName) { if (p->name[0] != '\0') { model = p->name; } @@ -1011,7 +1011,7 @@ void launchBTDiscovery(bool overrideDiscovery) { Log.trace(F("Unit: %s"), prop.value()["unit"].as()); Log.trace(F("Name: %s"), prop.value()["name"].as()); String entity_name = ""; - if (displayDeviceName) { + if (displayDeviceName || ForceDeviceName) { entity_name = String(model.c_str()) + "-" + String(prop.key().c_str()); } else { entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); diff --git a/main/mqttDiscovery.cpp b/main/mqttDiscovery.cpp index 4c7b52ef13..64fcea91f8 100644 --- a/main/mqttDiscovery.cpp +++ b/main/mqttDiscovery.cpp @@ -587,9 +587,11 @@ void createDiscovery(const char* sensor_type, // generate unique device name by adding the second half of the device_id only if device_name and device_id are different and we don't want to use the BLE name if (device_name[0]) { #if defined(ZwebUI) && defined(ESP32) // displayDeviceName only applies when running with the WebUI and ESP32 - if (strcmp(device_id, device_name) != 0 && device_id[0] && !displayDeviceName) { + if (strcmp(device_id, device_name) != 0 && device_id[0] && !displayDeviceName) { + #elif !ForceDeviceName // Support ForceDeviceName for esp8266's + if (strcmp(device_id, device_name) != 0 && device_id[0] && !ForceDeviceName) { #else - if (strcmp(device_id, device_name) != 0 && device_id[0]) { + if (strcmp(device_id, device_name) != 0 && device_id[0]) { #endif device["name"] = device_name + String("-") + String(device_id + 6); } else { From 90f13606576281d754cf02d2603b191d4108304f Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 3 Aug 2025 14:31:54 +1200 Subject: [PATCH 04/24] Changing WebUI to include display device name, and change it to select drop down rather than checkbox --- main/webUI.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/main/webUI.cpp b/main/webUI.cpp index bbbfa3415d..b0a4ab2e60 100644 --- a/main/webUI.cpp +++ b/main/webUI.cpp @@ -518,16 +518,12 @@ void handleWU() { bool update = false; if (server.hasArg("dm") && server.arg("dm").toInt() != displayMetric) { - WEBUI_TRACE_LOG(F("handleWU Update displayMetric from: %d" CR), displayMetric); displayMetric = server.arg("dm").toInt(); - WEBUI_TRACE_LOG(F("handleWU Update displayMetric to: %d" CR), displayMetric); update = true; } if (server.hasArg("dn") && server.arg("dn").toInt() != displayDeviceName) { - WEBUI_TRACE_LOG(F("handleWU Update displayDeviceName from: %d" CR), displayDeviceName); displayDeviceName = server.arg("dn").toInt(); - WEBUI_TRACE_LOG(F("handleWU Update displayDeviceName from: %d" CR), displayDeviceName); update = true; } From 8fb8f4141ee895dc895a510ff7d5b1a6d3a48213 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 3 Aug 2025 15:21:25 +1200 Subject: [PATCH 05/24] Fixes for WebUI and BT for supporting custom setting Display name --- main/config_BT.h | 4 ++++ main/config_WebUI.h | 4 ---- main/gatewayBT.cpp | 1 + main/mqttDiscovery.cpp | 4 +--- main/webUI.cpp | 2 -- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/main/config_BT.h b/main/config_BT.h index 5105110139..67edbcdca4 100644 --- a/main/config_BT.h +++ b/main/config_BT.h @@ -150,6 +150,10 @@ extern unsigned long scanCount; # define enableMultiGTWSync true // //define true to use tracker and closest control devices sync across OpenMQTTGateway and Theengs Gateway gateways #endif +#ifndef DISPLAY_DEVICE_NAME +# define DISPLAY_DEVICE_NAME false // Set to true to force the device name to be from the name of the device and not the model +#endif + /*--------------HOME ASSISTANT ROOM PRESENCE--------------*/ #define subjectHomePresence "presence/" // will send Home Assistant room presence message to this topic (first part is same for all rooms, second is room name) diff --git a/main/config_WebUI.h b/main/config_WebUI.h index 5f1cef6edc..5ba02a4261 100644 --- a/main/config_WebUI.h +++ b/main/config_WebUI.h @@ -42,10 +42,6 @@ # define DISPLAY_METRIC true // Units used for display of sensor data #endif -#ifndef DISPLAY_DEVICE_NAME -# define DISPLAY_DEVICE_NAME false // Set to true to force the device name to be from the name of the device and not the model -#endif - #ifndef DISPLAY_WEBUI_INTERVAL # define DISPLAY_WEBUI_INTERVAL 3 // Number of seconds between json message displays #endif diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index fbe5958fa8..2560dbb3dc 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -88,6 +88,7 @@ static bool oneWhite = false; extern bool BTProcessLock; extern int queueLength; +boolean displayDeviceName = DISPLAY_DEVICE_NAME; void setupBTTasksAndBLE(); bool checkIfIsTracker(char ch); diff --git a/main/mqttDiscovery.cpp b/main/mqttDiscovery.cpp index 64fcea91f8..fb75a9e302 100644 --- a/main/mqttDiscovery.cpp +++ b/main/mqttDiscovery.cpp @@ -588,10 +588,8 @@ void createDiscovery(const char* sensor_type, if (device_name[0]) { #if defined(ZwebUI) && defined(ESP32) // displayDeviceName only applies when running with the WebUI and ESP32 if (strcmp(device_id, device_name) != 0 && device_id[0] && !displayDeviceName) { - #elif !ForceDeviceName // Support ForceDeviceName for esp8266's + #else !ForceDeviceName // Support ForceDeviceName for esp8266's if (strcmp(device_id, device_name) != 0 && device_id[0] && !ForceDeviceName) { - #else - if (strcmp(device_id, device_name) != 0 && device_id[0]) { #endif device["name"] = device_name + String("-") + String(device_id + 6); } else { diff --git a/main/webUI.cpp b/main/webUI.cpp index b0a4ab2e60..0eb68cd8e4 100644 --- a/main/webUI.cpp +++ b/main/webUI.cpp @@ -85,7 +85,6 @@ const char* www_username = WEBUI_LOGIN; String authFailResponse = "Authentication Failed"; bool webUISecure = WEBUI_AUTH; boolean displayMetric = DISPLAY_METRIC; -boolean displayDeviceName = DISPLAY_DEVICE_NAME; /*********************************************************************************************\ * ESP32 AutoMutex @@ -1808,7 +1807,6 @@ bool WebUIConfig_save() { void WebUIConfig_init() { displayMetric = DISPLAY_METRIC; - displayDeviceName = DISPLAY_DEVICE_NAME; webUISecure = WEBUI_AUTH; Log.notice(F("WebUI config initialised" CR)); } From 98174615547f590ac7fd91f0096fac22c4390819 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 3 Aug 2025 15:30:10 +1200 Subject: [PATCH 06/24] Fixes for WebUI and BT for supporting custom setting Display name --- main/mqttDiscovery.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/mqttDiscovery.cpp b/main/mqttDiscovery.cpp index fb75a9e302..cd2835b392 100644 --- a/main/mqttDiscovery.cpp +++ b/main/mqttDiscovery.cpp @@ -586,7 +586,7 @@ void createDiscovery(const char* sensor_type, // generate unique device name by adding the second half of the device_id only if device_name and device_id are different and we don't want to use the BLE name if (device_name[0]) { - #if defined(ZwebUI) && defined(ESP32) // displayDeviceName only applies when running with the WebUI and ESP32 + #if defined(ZgatewayBT) // displayDeviceName only applies when running with the WebUI and ESP32 if (strcmp(device_id, device_name) != 0 && device_id[0] && !displayDeviceName) { #else !ForceDeviceName // Support ForceDeviceName for esp8266's if (strcmp(device_id, device_name) != 0 && device_id[0] && !ForceDeviceName) { From 966c2344fa8d133a6f6050cc0b02369442f1c840 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 3 Aug 2025 15:38:14 +1200 Subject: [PATCH 07/24] Move DISPLAY_DEVICE_NAME to User_config.h --- main/User_config.h | 5 +++++ main/config_BT.h | 4 ---- main/gatewayBT.cpp | 1 - main/main.cpp | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/main/User_config.h b/main/User_config.h index ea5fcbf4bd..16e07c01fd 100644 --- a/main/User_config.h +++ b/main/User_config.h @@ -604,6 +604,11 @@ extern ss_cnt_parameters cnt_parameters_array[]; # define LOG_LEVEL LOG_LEVEL_NOTICE #endif +/*-------------------DEFINE DISPLAY NAME-------------------*/ +#ifndef DISPLAY_DEVICE_NAME +# define DISPLAY_DEVICE_NAME false // Set to true to force the device name to be from the name of the device and not the model +#endif + /*-------------------ESP Wifi band and tx power ---------------------*/ //Certain sensors are sensitive to Wifi which can cause interference with their normal operation //For example it can cause false triggers on a PIR HC-SR501 diff --git a/main/config_BT.h b/main/config_BT.h index 67edbcdca4..5105110139 100644 --- a/main/config_BT.h +++ b/main/config_BT.h @@ -150,10 +150,6 @@ extern unsigned long scanCount; # define enableMultiGTWSync true // //define true to use tracker and closest control devices sync across OpenMQTTGateway and Theengs Gateway gateways #endif -#ifndef DISPLAY_DEVICE_NAME -# define DISPLAY_DEVICE_NAME false // Set to true to force the device name to be from the name of the device and not the model -#endif - /*--------------HOME ASSISTANT ROOM PRESENCE--------------*/ #define subjectHomePresence "presence/" // will send Home Assistant room presence message to this topic (first part is same for all rooms, second is room name) diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index 2560dbb3dc..fbe5958fa8 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -88,7 +88,6 @@ static bool oneWhite = false; extern bool BTProcessLock; extern int queueLength; -boolean displayDeviceName = DISPLAY_DEVICE_NAME; void setupBTTasksAndBLE(); bool checkIfIsTracker(char ch); diff --git a/main/main.cpp b/main/main.cpp index bd2b379f45..d4341a58eb 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -286,6 +286,7 @@ bool failSafeMode = false; bool ProcessLock = true; // Process lock when we want to use a critical function like OTA for example bool mqttSetupPending = true; static int cnt_index = CNT_DEFAULT_INDEX; +boolean displayDeviceName = DISPLAY_DEVICE_NAME; #ifdef ESP32 # include From 56f1bb2e48a4d33c7a3fe3f754c1f6c9eb6d4ea6 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 3 Aug 2025 15:58:24 +1200 Subject: [PATCH 08/24] Update docs to include change for Display temperature --- docs/use/displays.md | 6 +++--- docs/use/webui.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/use/displays.md b/docs/use/displays.md index 572a56523d..34e46ec850 100644 --- a/docs/use/displays.md +++ b/docs/use/displays.md @@ -45,10 +45,10 @@ or with the runtime command `mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoSSD1306/config -m {"brightness":50}` -### Metric or Imperial property units -To have applicable device properties displayed in Imperial units, e.g. °F for temperature. +### Celsius or Fahrenheit property units +To have applicable device temperature properties displayed in °C Celsius or °F Fahrenheit. -This can be set with the compiler directive `-DDISPLAY_METRIC=false`. +This can be set with the compiler directive `-DDISPLAY_METRIC=false` for Fahrenheit or via the Configure WebUI Property `Display temperature` As the display Metric setting is being defined in the WebUI part of OpenMQTTGateway changes need to be sent there with the runtime command diff --git a/docs/use/webui.md b/docs/use/webui.md index 24198c0484..d763f9e2fb 100644 --- a/docs/use/webui.md +++ b/docs/use/webui.md @@ -28,7 +28,7 @@ Ability to change the mqtt settings, if the change is unsuccessful it will rever ## WebUI -Ability to change the display of sensor to Metric or Imperial, and disable the WebUI Authentication +Ability to change the display of temperature sensors to Celsius or Fahrenheit, display the advertised device name or the device model_id and disable the WebUI Authentication ## Logging From f9df09063acd962774bb2096b2f7d30089f1a1ed Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 3 Aug 2025 16:07:41 +1200 Subject: [PATCH 09/24] Update docs to include change for Display temperature --- docs/use/displays.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/use/displays.md b/docs/use/displays.md index 34e46ec850..99cc66e70e 100644 --- a/docs/use/displays.md +++ b/docs/use/displays.md @@ -54,6 +54,16 @@ As the display Metric setting is being defined in the WebUI part of OpenMQTTGate `mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoWebUI/config -m {"displayMetric":false}` +### Display name as Bluetooth Name or `model_id` +There is a build property of ForceDeviceName which forces devices when they are added in Home Assistant auto-discovery to be created with their Bluetooth advertised name isntead of their `model_id`. The default naming is `model_id` with `{"displayDeviceName":true}`. + +This can also be adjusted in the WebUI by switching the Configure WebUI Device naming between `Model ID` (false) or `Device name` (true) + +This can also be changed with the runtime command. + +`mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoWebUI/config -m {"displayDeviceName":true}` + + ### Rotating the display by 180 degrees This can be set with the compiler directive `-DDISPLAY_FLIP=false`. From 6ff9d61ccfa537994ae4e9699a31692b0b94463f Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 3 Aug 2025 17:34:06 +1200 Subject: [PATCH 10/24] Fix minor cosmetic bug where devices were not linking in HA to the gateway using via_device as it should be the gateway mac address not name --- main/mqttDiscovery.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main/mqttDiscovery.cpp b/main/mqttDiscovery.cpp index cd2835b392..0cf09aa212 100644 --- a/main/mqttDiscovery.cpp +++ b/main/mqttDiscovery.cpp @@ -43,6 +43,7 @@ extern bool ethConnected; extern JsonArray modules; +String gateway_mac; char discovery_prefix[parameters_size + 1] = discovery_Prefix; // From https://github.com/home-assistant/core/blob/d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf/homeassistant/const.py#L225 @@ -564,6 +565,7 @@ void createDiscovery(const char* sensor_type, device["sw"] = OMG_VERSION; identifiers.add(String(getMacAddress())); + gateway_mac = getMacAddress(); } else { //The Connections if (device_id[0]) { @@ -597,7 +599,7 @@ void createDiscovery(const char* sensor_type, } } - device["via_device"] = String(gateway_name); //device name of the board + device["via_device"] = String(gateway_mac); //mac address of the gateway so that the devices link to the gateway } sensor["device"] = device; From 51a75254cb8fe180a00254d25280a14f129f7757 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Thu, 7 Aug 2025 11:43:15 +1200 Subject: [PATCH 11/24] Roll back displayMetric and gateway_mac to separate this out to just displayDeviceName --- docs/use/displays.md | 6 +++--- main/config_WebContent.h | 2 +- main/main.cpp | 2 +- main/mqttDiscovery.cpp | 4 +--- main/webUI.cpp | 18 +++++++++--------- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/docs/use/displays.md b/docs/use/displays.md index 99cc66e70e..cb3b49b35c 100644 --- a/docs/use/displays.md +++ b/docs/use/displays.md @@ -45,10 +45,10 @@ or with the runtime command `mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoSSD1306/config -m {"brightness":50}` -### Celsius or Fahrenheit property units -To have applicable device temperature properties displayed in °C Celsius or °F Fahrenheit. +### Metric or Imperial property units +To have applicable device properties displayed in Imperial units, e.g. °F for temperature. -This can be set with the compiler directive `-DDISPLAY_METRIC=false` for Fahrenheit or via the Configure WebUI Property `Display temperature` +This can be set with the compiler directive `-DDISPLAY_METRIC=false`. As the display Metric setting is being defined in the WebUI part of OpenMQTTGateway changes need to be sent there with the runtime command diff --git a/main/config_WebContent.h b/main/config_WebContent.h index 4544d3f394..a525f8f009 100644 --- a/main/config_WebContent.h +++ b/main/config_WebContent.h @@ -112,7 +112,7 @@ const char config_gateway_body[] = body_header "
OpenMQTTGateway Logging

Log Level


" body_footer_config_menu; -const char config_webui_body[] = body_header "
Configure WebUI

Display temperature

Device naming

Secure WebUI


" body_footer_config_menu; +const char config_webui_body[] = body_header "
Configure WebUI

Display Metric

Device naming

Secure WebUI


" body_footer_config_menu; const char config_rf_body[] = body_header "
" diff --git a/main/main.cpp b/main/main.cpp index d4341a58eb..0238bcae7a 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -286,7 +286,7 @@ bool failSafeMode = false; bool ProcessLock = true; // Process lock when we want to use a critical function like OTA for example bool mqttSetupPending = true; static int cnt_index = CNT_DEFAULT_INDEX; -boolean displayDeviceName = DISPLAY_DEVICE_NAME; +bool displayDeviceName = DISPLAY_DEVICE_NAME; #ifdef ESP32 # include diff --git a/main/mqttDiscovery.cpp b/main/mqttDiscovery.cpp index 0cf09aa212..cd2835b392 100644 --- a/main/mqttDiscovery.cpp +++ b/main/mqttDiscovery.cpp @@ -43,7 +43,6 @@ extern bool ethConnected; extern JsonArray modules; -String gateway_mac; char discovery_prefix[parameters_size + 1] = discovery_Prefix; // From https://github.com/home-assistant/core/blob/d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf/homeassistant/const.py#L225 @@ -565,7 +564,6 @@ void createDiscovery(const char* sensor_type, device["sw"] = OMG_VERSION; identifiers.add(String(getMacAddress())); - gateway_mac = getMacAddress(); } else { //The Connections if (device_id[0]) { @@ -599,7 +597,7 @@ void createDiscovery(const char* sensor_type, } } - device["via_device"] = String(gateway_mac); //mac address of the gateway so that the devices link to the gateway + device["via_device"] = String(gateway_name); //device name of the board } sensor["device"] = device; diff --git a/main/webUI.cpp b/main/webUI.cpp index 0eb68cd8e4..18dedc2ce8 100644 --- a/main/webUI.cpp +++ b/main/webUI.cpp @@ -502,10 +502,10 @@ void handleCN() { /** * @brief /WU - Configuration Page * T: handleWU: uri: /wu, args: 3, method: 1 - * T: handleWU Arg: 0, dm=1 - displayMetric - * T: handleWU Arg: 0, dn=1 - displayDeviceName - * T: handleWU Arg: 1, sw=on - webUISecure - * T: handleWU Arg: 2, save= + * T: handleWU Arg: 0, dm=on - displayMetric + * T: handleWU Arg: 1, dn=1 - displayDeviceName + * T: handleWU Arg: 2, sw=on - webUISecure + * T: handleWU Arg: 3, save= */ void handleWU() { WEBUI_TRACE_LOG(F("handleWU: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method()); @@ -516,10 +516,10 @@ void handleWU() { } bool update = false; - if (server.hasArg("dm") && server.arg("dm").toInt() != displayMetric) { - displayMetric = server.arg("dm").toInt(); - update = true; - } + if (displayMetric != server.hasArg("dm")) { + update = true; + } + displayMetric = server.hasArg("dm"); if (server.hasArg("dn") && server.arg("dn").toInt() != displayDeviceName) { displayDeviceName = server.arg("dn").toInt(); @@ -546,7 +546,7 @@ void handleWU() { response += String(script); response += String(style); int logLevel = Log.getLevel(); - snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_webui_body, jsonChar, gateway_name, (displayMetric ? "selected" : ""), (!displayMetric ? "selected" : ""), (!displayDeviceName ? "selected" : ""), (displayDeviceName ? "selected" : ""), (webUISecure ? "checked" : "")); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_webui_body, jsonChar, gateway_name, (displayMetric ? "checked" : ""), (!displayDeviceName ? "selected" : ""), (displayDeviceName ? "selected" : ""), (webUISecure ? "checked" : "")); response += String(buffer); snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION); response += String(buffer); From 8654f57bc07aa954a8458b4e9d4c0d732c757af6 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Thu, 7 Aug 2025 14:54:38 +1200 Subject: [PATCH 12/24] Add displayDeviceName as a config variable. --- docs/use/displays.md | 2 +- docs/use/webui.md | 2 +- main/webUI.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/use/displays.md b/docs/use/displays.md index cb3b49b35c..3e1c1f8e81 100644 --- a/docs/use/displays.md +++ b/docs/use/displays.md @@ -54,7 +54,7 @@ As the display Metric setting is being defined in the WebUI part of OpenMQTTGate `mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoWebUI/config -m {"displayMetric":false}` -### Display name as Bluetooth Name or `model_id` +### Display name as advetised Bluetooth name or `model_id` There is a build property of ForceDeviceName which forces devices when they are added in Home Assistant auto-discovery to be created with their Bluetooth advertised name isntead of their `model_id`. The default naming is `model_id` with `{"displayDeviceName":true}`. This can also be adjusted in the WebUI by switching the Configure WebUI Device naming between `Model ID` (false) or `Device name` (true) diff --git a/docs/use/webui.md b/docs/use/webui.md index d763f9e2fb..c881ca8778 100644 --- a/docs/use/webui.md +++ b/docs/use/webui.md @@ -28,7 +28,7 @@ Ability to change the mqtt settings, if the change is unsuccessful it will rever ## WebUI -Ability to change the display of temperature sensors to Celsius or Fahrenheit, display the advertised device name or the device model_id and disable the WebUI Authentication +Ability to change the display of sensor to Metric or Imperial, display the device name as the Bluetooth advertised device name or the model id and disable the WebUI Authentication ## Logging diff --git a/main/webUI.cpp b/main/webUI.cpp index 18dedc2ce8..cd8e0601a3 100644 --- a/main/webUI.cpp +++ b/main/webUI.cpp @@ -517,8 +517,8 @@ void handleWU() { bool update = false; if (displayMetric != server.hasArg("dm")) { - update = true; - } + update = true; + } displayMetric = server.hasArg("dm"); if (server.hasArg("dn") && server.arg("dn").toInt() != displayDeviceName) { From 667c3dcc89c0ee9aba1d25f8331ebb5fdb7b54c3 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sat, 9 Aug 2025 22:52:54 +1200 Subject: [PATCH 13/24] Disable stateBTMeasures(false) as it casues a segfault --- main/gatewayBT.cpp | 3676 ++++++++++++++++++++++---------------------- 1 file changed, 1838 insertions(+), 1838 deletions(-) diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index 2a51c99ddc..99a50771d1 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -1,1838 +1,1838 @@ -/* - OpenMQTTGateway - ESP8266 or Arduino program for home automation - - Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker - Send and receiving command by MQTT - - This gateway enables to: - - publish MQTT data to a topic related to BLE devices data - - Copyright: (c)Florian ROBERT - - This file is part of OpenMQTTGateway. - - OpenMQTTGateway is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - OpenMQTTGateway is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Thanks to wolass https://github.com/wolass for suggesting me HM 10 and dinosd https://github.com/dinosd/BLE_PROXIMITY for inspiring me how to implement the gateway -*/ -#include "User_config.h" - -#ifdef ZgatewayBT -# include "TheengsCommon.h" - -SemaphoreHandle_t semaphoreCreateOrUpdateDevice; -SemaphoreHandle_t semaphoreBLEOperation; -QueueHandle_t BLEQueue; -unsigned long scanCount = 0; -# include -# include -# include -# include -# include -# include - -# include - -# include "TheengsCommon.h" -# include "config_mqttDiscovery.h" -# include "gatewayBLEConnect.h" -# include "soc/timer_group_reg.h" -# include "soc/timer_group_struct.h" - -using namespace std; - -// Global struct to store live BT configuration data -BTConfig_s BTConfig; - -# if BLEDecoder -# include -# if BLEDecryptor -# include "mbedtls/ccm.h" -# include "mbedtls/aes.h" -# endif -TheengsDecoder decoder; -# endif - -static TaskHandle_t xCoreTaskHandle; -static TaskHandle_t xProcBLETaskHandle; - -struct decompose { - int start; - int len; - bool reverse; -}; - -vector BLEactions; - -vector devices; -int newDevices = 0; - -static BLEdevice NO_BT_DEVICE_FOUND = { - {0}, - 0, - false, - false, - false, - false, - (char)UNKWNON_MODEL, - 0, -}; -static bool oneWhite = false; - -extern bool BTProcessLock; -extern int queueLength; - -void setupBTTasksAndBLE(); -bool checkIfIsTracker(char ch); -void hass_presence(JsonObject& HomePresence); -void BTforceScan(); - -void BTConfig_init() { - BTConfig.bleConnect = AttemptBLEConnect; - BTConfig.BLEinterval = TimeBtwRead; - BTConfig.adaptiveScan = AdaptiveBLEScan; - BTConfig.intervalActiveScan = TimeBtwActive; - BTConfig.intervalConnect = TimeBtwConnect; - BTConfig.scanDuration = Scan_duration; - BTConfig.pubOnlySensors = PublishOnlySensors; - BTConfig.pubRandomMACs = PublishRandomMACs; - BTConfig.presenceEnable = HassPresence; - BTConfig.presenceTopic = subjectHomePresence; - BTConfig.presenceUseBeaconUuid = useBeaconUuidForPresence; - BTConfig.minRssi = MinimumRSSI; - BTConfig.extDecoderEnable = UseExtDecoder; - BTConfig.extDecoderTopic = MQTTDecodeTopic; - BTConfig.filterConnectable = BLE_FILTER_CONNECTABLE; - BTConfig.pubAdvData = pubBLEAdvData; - BTConfig.pubBeaconUuidForTopic = useBeaconUuidForTopic; - BTConfig.ignoreWBlist = false; - BTConfig.presenceAwayTimer = PresenceAwayTimer; - BTConfig.movingTimer = MovingTimer; - BTConfig.forcePassiveScan = false; - BTConfig.enabled = EnableBT; -} - -unsigned long timeBetweenConnect = 0; -unsigned long timeBetweenActive = 0; - -String stateBTMeasures(bool start) { - StaticJsonDocument jsonBuffer; - JsonObject jo = jsonBuffer.to(); - jo["bleconnect"] = BTConfig.bleConnect; - jo["interval"] = BTConfig.BLEinterval; - jo["adaptivescan"] = BTConfig.adaptiveScan; - jo["intervalacts"] = BTConfig.intervalActiveScan; - jo["intervalcnct"] = BTConfig.intervalConnect; - jo["scanduration"] = BTConfig.scanDuration; - jo["hasspresence"] = BTConfig.presenceEnable; - jo["prestopic"] = BTConfig.presenceTopic; - jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; - jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value - jo["extDecoderEnable"] = BTConfig.extDecoderEnable; - jo["extDecoderTopic"] = BTConfig.extDecoderTopic; - jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; - jo["ignoreWBlist"] = BTConfig.ignoreWBlist; - jo["forcepscn"] = BTConfig.forcePassiveScan; - jo["tskstck"] = uxTaskGetStackHighWaterMark(xProcBLETaskHandle); - jo["crstck"] = uxTaskGetStackHighWaterMark(xCoreTaskHandle); - jo["enabled"] = BTConfig.enabled; - jo["scnct"] = scanCount; -# if BLEDecoder - jo["onlysensors"] = BTConfig.pubOnlySensors; - jo["randommacs"] = BTConfig.pubRandomMACs; - jo["filterConnectable"] = BTConfig.filterConnectable; - jo["pubadvdata"] = BTConfig.pubAdvData; - jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; - jo["movingtimer"] = BTConfig.movingTimer; -# endif - - if (start) { - Log.notice(F("BT sys: ")); - serializeJsonPretty(jsonBuffer, Serial); - Serial.println(); - return ""; // Do not try to erase/write/send config at startup - } - String output; - serializeJson(jo, output); - jo["origin"] = subjectBTtoMQTT; - enqueueJsonObject(jo, QueueSemaphoreTimeOutTask); - return (output); -} - -void BTConfig_fromJson(JsonObject& BTdata, bool startup = false) { - // Attempts to connect to eligible devices or not - Config_update(BTdata, "bleconnect", BTConfig.bleConnect); - // Identify AdaptiveScan deactivation to pass to continuous mode or activation to come back to default settings - if (startup == false) { - if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == false && BTConfig.presenceEnable == true) { - BTdata["adaptivescan"] = true; - } else if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == true && BTConfig.presenceEnable == false) { - BTdata["adaptivescan"] = false; - } - - if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == false && BTConfig.adaptiveScan == true) { - BTdata["interval"] = MinTimeBtwScan; - BTdata["intervalacts"] = MinTimeBtwScan; - BTdata["scanduration"] = MinScanDuration; - } else if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == true && BTConfig.adaptiveScan == false) { - BTdata["interval"] = TimeBtwRead; - BTdata["intervalacts"] = TimeBtwActive; - BTdata["scanduration"] = Scan_duration; - } - // Identify if the gateway is enabled or not and stop start accordingly - if (BTdata.containsKey("enabled") && BTdata["enabled"] == false && BTConfig.enabled == true) { - // Stop the gateway but without deinit to enable a future BT restart - stopProcessing(false); - } else if (BTdata.containsKey("enabled") && BTdata["enabled"] == true && BTConfig.enabled == false) { - BTProcessLock = false; - setupBTTasksAndBLE(); - } - } - // Home Assistant presence message - Config_update(BTdata, "hasspresence", BTConfig.presenceEnable); - // Time before before active scan - // Scan interval set - and avoid intervalacts to be lower than interval - if (BTdata.containsKey("interval") && BTdata["interval"] != 0) { - BTConfig.adaptiveScan = false; - Config_update(BTdata, "interval", BTConfig.BLEinterval); - if (BTConfig.intervalActiveScan < BTConfig.BLEinterval) { - Config_update(BTdata, "interval", BTConfig.intervalActiveScan); - } - } - // Define if the scan is adaptive or not - and avoid intervalacts to be lower than interval - if (BTdata.containsKey("intervalacts") && BTdata["intervalacts"] < BTConfig.BLEinterval) { - BTConfig.adaptiveScan = false; - // Config_update(BTdata, "interval", BTConfig.intervalActiveScan); - BTConfig.intervalActiveScan = BTConfig.BLEinterval; - } else { - Config_update(BTdata, "intervalacts", BTConfig.intervalActiveScan); - } - // Adaptive scan set - Config_update(BTdata, "adaptivescan", BTConfig.adaptiveScan); - // Time before a connect set - Config_update(BTdata, "intervalcnct", BTConfig.intervalConnect); - // publish all BLE devices discovered or only the identified sensors (like temperature sensors) - Config_update(BTdata, "scanduration", BTConfig.scanDuration); - // define the duration for a scan; in milliseconds - Config_update(BTdata, "onlysensors", BTConfig.pubOnlySensors); - // publish devices which randomly change their MAC addresses - Config_update(BTdata, "randommacs", BTConfig.pubRandomMACs); - // Home Assistant presence message topic - Config_update(BTdata, "prestopic", BTConfig.presenceTopic); - // Home Assistant presence message use iBeacon UUID - Config_update(BTdata, "presuseuuid", BTConfig.presenceUseBeaconUuid); - // Timer to trigger a device state as offline if not seen - Config_update(BTdata, "presenceawaytimer", BTConfig.presenceAwayTimer); - // Timer to trigger a device state as offline if not seen - Config_update(BTdata, "movingtimer", BTConfig.movingTimer); - // Force passive scan - Config_update(BTdata, "forcepscn", BTConfig.forcePassiveScan); - // MinRSSI set - Config_update(BTdata, "minrssi", BTConfig.minRssi); - // Send undecoded device data - Config_update(BTdata, "extDecoderEnable", BTConfig.extDecoderEnable); - // Topic to send undecoded device data - Config_update(BTdata, "extDecoderTopic", BTConfig.extDecoderTopic); - // Sets whether to filter publishing - Config_update(BTdata, "filterConnectable", BTConfig.filterConnectable); - // Publish advertisement data - Config_update(BTdata, "pubadvdata", BTConfig.pubAdvData); - // Use iBeacon UUID as topic, instead of sender (random) MAC address - Config_update(BTdata, "pubuuid4topic", BTConfig.pubBeaconUuidForTopic); - // Disable Whitelist & Blacklist - Config_update(BTdata, "ignoreWBlist", (BTConfig.ignoreWBlist)); - // Enable or disable the BT gateway - Config_update(BTdata, "enabled", BTConfig.enabled); - - stateBTMeasures(startup); - - if (BTdata.containsKey("erase") && BTdata["erase"].as()) { - // Erase config from NVS (non-volatile storage) - preferences.begin(Gateway_Short_Name, false); - if (preferences.isKey("BTConfig")) { - int result = preferences.remove("BTConfig"); - Log.notice(F("BT config erase result: %d" CR), result); - preferences.end(); - return; // Erase prevails on save, so skipping save - } else { - preferences.end(); - Log.notice(F("BT config not found" CR)); - } - } - - if (BTdata.containsKey("save") && BTdata["save"].as()) { - StaticJsonDocument jsonBuffer; - JsonObject jo = jsonBuffer.to(); - jo["bleconnect"] = BTConfig.bleConnect; - jo["interval"] = BTConfig.BLEinterval; - jo["adaptivescan"] = BTConfig.adaptiveScan; - jo["intervalacts"] = BTConfig.intervalActiveScan; - jo["intervalcnct"] = BTConfig.intervalConnect; - jo["scanduration"] = BTConfig.scanDuration; - jo["onlysensors"] = BTConfig.pubOnlySensors; - jo["randommacs"] = BTConfig.pubRandomMACs; - jo["hasspresence"] = BTConfig.presenceEnable; - jo["prestopic"] = BTConfig.presenceTopic; - jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; - jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value - jo["extDecoderEnable"] = BTConfig.extDecoderEnable; - jo["extDecoderTopic"] = BTConfig.extDecoderTopic; - jo["filterConnectable"] = BTConfig.filterConnectable; - jo["pubadvdata"] = BTConfig.pubAdvData; - jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; - jo["ignoreWBlist"] = BTConfig.ignoreWBlist; - jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; - jo["movingtimer"] = BTConfig.movingTimer; - jo["forcepscn"] = BTConfig.forcePassiveScan; - jo["enabled"] = BTConfig.enabled; - // Save config into NVS (non-volatile storage) - String conf = ""; - serializeJson(jsonBuffer, conf); - preferences.begin(Gateway_Short_Name, false); - int result = preferences.putString("BTConfig", conf); - preferences.end(); - Log.notice(F("BT config save: %s, result: %d" CR), conf.c_str(), result); - } -} - -void BTConfig_load() { - StaticJsonDocument jsonBuffer; - preferences.begin(Gateway_Short_Name, true); - if (preferences.isKey("BTConfig")) { - auto error = deserializeJson(jsonBuffer, preferences.getString("BTConfig", "{}")); - preferences.end(); - Log.notice(F("BT config loaded" CR)); - if (error) { - Log.error(F("BT config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity()); - return; - } - if (jsonBuffer.isNull()) { - Log.warning(F("BT config is null" CR)); - return; - } - JsonObject jo = jsonBuffer.as(); - BTConfig_fromJson(jo, true); // Never send MQTT message with config - Log.notice(F("BT config loaded" CR)); - } else { - preferences.end(); - Log.notice(F("BT config not found" CR)); - } -} - -void PublishDeviceData(JsonObject& BLEdata); - -atomic_int forceBTScan; - -void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type = 0, const char* name = ""); - -BLEdevice* getDeviceByMac(const char* mac); // Declared here to avoid pre-compilation issue (misplaced auto declaration by pio) -BLEdevice* getDeviceByMac(const char* mac) { - Log.trace(F("getDeviceByMac %s" CR), mac); - - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - if ((strcmp((*it)->macAdr, mac) == 0)) { - return *it; - } - } - return &NO_BT_DEVICE_FOUND; -} - -bool updateWorB(JsonObject& BTdata, bool isWhite) { - Log.trace(F("update WorB" CR)); - const char* jsonKey = isWhite ? "white-list" : "black-list"; - - int size = BTdata[jsonKey].size(); - if (size == 0) - return false; - - for (int i = 0; i < size; i++) { - const char* mac = BTdata[jsonKey][i]; - createOrUpdateDevice(mac, (isWhite ? device_flags_isWhiteL : device_flags_isBlackL), - UNKWNON_MODEL); - } - - return true; -} - -void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type, const char* name) { - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(30000)) == pdFALSE) { - Log.error(F("Semaphore NOT taken" CR)); - return; - } - BLEdevice* device = getDeviceByMac(mac); - if (device == &NO_BT_DEVICE_FOUND) { - Log.trace(F("add %s" CR), mac); - //new device - device = new BLEdevice(); - strcpy(device->macAdr, mac); - device->isDisc = flags & device_flags_isDisc; - device->isWhtL = flags & device_flags_isWhiteL; - device->isBlkL = flags & device_flags_isBlackL; - device->connect = flags & device_flags_connect; - device->macType = mac_type; - // Check name length - if (strlen(name) > 20) { - Log.warning(F("Name too long, truncating" CR)); - strncpy(device->name, name, 20); - device->name[19] = '\0'; - } else { - strcpy(device->name, name); - } - device->sensorModel_id = model; - device->lastUpdate = millis(); - devices.push_back(device); - newDevices++; - } else { - Log.trace(F("update %s" CR), mac); - device->lastUpdate = millis(); - device->macType = mac_type; - - if (flags & device_flags_isDisc) { - device->isDisc = true; - } - - if (flags & device_flags_connect) { - device->connect = true; - } - - if (model != UNKWNON_MODEL && device->sensorModel_id == UNKWNON_MODEL) { - newDevices++; - device->isDisc = false; - device->sensorModel_id = model; - } - - // If a device has been added to the white-list, flag it so it can be auto-detected - if (!device->isWhtL && flags & device_flags_isWhiteL) { - newDevices++; - } - if (flags & device_flags_isWhiteL || flags & device_flags_isBlackL) { - device->isWhtL = flags & device_flags_isWhiteL; - device->isBlkL = flags & device_flags_isBlackL; - } - } - - // update oneWhite flag - oneWhite = oneWhite || device->isWhtL; - - xSemaphoreGive(semaphoreCreateOrUpdateDevice); -} - -void updateDevicesStatus() { - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - BLEdevice* p = *it; - unsigned long now = millis(); - // Check for tracker status - bool isTracker = false; -# if BLEDecoder - std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); - if (tag.length() >= 4) { - isTracker = checkIfIsTracker(tag[3]); - } - // Device tracker devices - if (isTracker) { // We apply the offline status only for tracking device, can be extended further to all the devices - if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.presenceAwayTimer) && (now > BTConfig.presenceAwayTimer)) && - (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = p->macAdr; - BLEdata["state"] = "offline"; - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - // We set the lastUpdate to 0 to avoid replublishing the offline state - p->lastUpdate = 0; - } - } - // Moving detection devices (devices with an accelerometer) - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BC08) { - if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.movingTimer) && (now > BTConfig.movingTimer)) && - (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = p->macAdr; - BLEdata["state"] = "offline"; - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - // We set the lastUpdate to 0 to avoid replublishing the offline state - p->lastUpdate = 0; - } - } -# endif - } -} - -void dumpDevices() { -# if LOG_LEVEL > LOG_LEVEL_NOTICE - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - BLEdevice* p = *it; - Log.trace(F("macAdr %s" CR), p->macAdr); - Log.trace(F("macType %d" CR), p->macType); - Log.trace(F("isDisc %d" CR), p->isDisc); - Log.trace(F("isWhtL %d" CR), p->isWhtL); - Log.trace(F("isBlkL %d" CR), p->isBlkL); - Log.trace(F("connect %d" CR), p->connect); - Log.trace(F("sensorModel_id %d" CR), p->sensorModel_id); - Log.trace(F("LastUpdate %u" CR), p->lastUpdate); - } -# endif -} - -void strupp(char* beg) { - while ((*beg = toupper(*beg))) - ++beg; -} - -# ifdef ZmqttDiscovery -void DT24Discovery(const char* mac, const char* sensorModel_id) { -# define DT24parametersCount 7 - Log.trace(F("DT24Discovery" CR)); - const char* DT24sensor[DT24parametersCount][9] = { - {"sensor", "volt", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "amp", mac, "current", jsonCurrent, "", "", "A", stateClassMeasurement}, - {"sensor", "watt", mac, "power", jsonPower, "", "", "W", stateClassMeasurement}, - {"sensor", "watt-hour", mac, "power", jsonEnergy, "", "", "kWh", stateClassMeasurement}, - {"sensor", "price", mac, "", jsonMsg, "", "", "", stateClassNone}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"binary_sensor", "inUse", mac, "power", jsonInuse, "", "", "", stateClassNone} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, DT24sensor, DT24parametersCount, "DT24", "ATorch", sensorModel_id); -} - -void BM2Discovery(const char* mac, const char* sensorModel_id) { -# define BM2parametersCount 2 - Log.trace(F("BM2Discovery" CR)); - const char* BM2sensor[BM2parametersCount][9] = { - {"sensor", "volt", mac, "voltage", jsonVoltBM2, "", "", "V", stateClassMeasurement}, // We use a json definition that retrieve only data from the BM2 decoder, as this sensor also advertize volt as an iBeacon - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, BM2sensor, BM2parametersCount, "BM2", "Generic", sensorModel_id); -} - -void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) { -# define LYWSD03MMCparametersCount 4 - Log.trace(F("LYWSD03MMCDiscovery" CR)); - const char* LYWSD03MMCsensor[LYWSD03MMCparametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, LYWSD03MMCsensor, LYWSD03MMCparametersCount, "LYWSD03MMC", "Xiaomi", sensorModel); -} - -void MHO_C401Discovery(const char* mac, const char* sensorModel) { -# define MHO_C401parametersCount 4 - Log.trace(F("MHO_C401Discovery" CR)); - const char* MHO_C401sensor[MHO_C401parametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, MHO_C401sensor, MHO_C401parametersCount, "MHO_C401", "Xiaomi", sensorModel); -} - -void HHCCJCY01HHCCDiscovery(const char* mac, const char* sensorModel) { -# define HHCCJCY01HHCCparametersCount 5 - Log.trace(F("HHCCJCY01HHCCDiscovery" CR)); - const char* HHCCJCY01HHCCsensor[HHCCJCY01HHCCparametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "lux", mac, "illuminance", jsonLux, "", "", "lx", stateClassMeasurement}, - {"sensor", "fer", mac, "", jsonFer, "", "", "µS/cm", stateClassMeasurement}, - {"sensor", "moi", mac, "", jsonMoi, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, HHCCJCY01HHCCsensor, HHCCJCY01HHCCparametersCount, "HHCCJCY01HHCC", "Xiaomi", sensorModel); -} - -void XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel) { -# define XMWSDJ04MMCparametersCount 4 - Log.trace(F("XMWSDJ04MMCDiscovery" CR)); - const char* XMWSDJ04MMCsensor[XMWSDJ04MMCparametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, XMWSDJ04MMCsensor, XMWSDJ04MMCparametersCount, "XMWSDJ04MMC", "Xiaomi", sensorModel); -} - -# else -void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) {} -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 XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel_id) {} -# endif - -/* - Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleScan.cpp - Ported to Arduino ESP32 by Evandro Copercini - */ -// core task implementation thanks to https://techtutorialsx.com/2017/05/09/esp32-running-code-on-a-specific-core/ - -//core on which the BLE detection task will run -static int taskCore = 0; - -class ScanCallbacks : public NimBLEScanCallbacks { - void onResult(const NimBLEAdvertisedDevice* advertisedDevice) { - NimBLEAdvertisedDevice* ad = new NimBLEAdvertisedDevice(*advertisedDevice); - if (xQueueSend(BLEQueue, &ad, 0) != pdTRUE) { - Log.error(F("BLEQueue full" CR)); - delete (ad); - } - } -} scanCallbacks; - -std::string convertServiceData(std::string deviceServiceData) { - int serviceDataLength = (int)deviceServiceData.length(); - char spr[2 * serviceDataLength + 1]; - for (int i = 0; i < serviceDataLength; i++) sprintf(spr + 2 * i, "%.2x", (unsigned char)deviceServiceData[i]); - spr[2 * serviceDataLength] = 0; - Log.trace(F("Converted service data (%d) to %s" CR), serviceDataLength, spr); - return spr; -} - -bool checkIfIsTracker(char ch) { - uint8_t data = 0; - if (ch >= '0' && ch <= '9') - data = ch - '0'; - else if (ch >= 'a' && ch <= 'f') - data = 10 + (ch - 'a'); - - if (((data >> 3) & 0x01) == 1) { - Log.trace(F("Is Device Tracker" CR)); - return true; - } else { - return false; - } -} - -void procBLETask(void* pvParameters) { - BLEAdvertisedDevice* advertisedDevice = nullptr; - - for (;;) { - xQueueReceive(BLEQueue, &advertisedDevice, portMAX_DELAY); - // Feed the watchdog - //esp_task_wdt_reset(); - if (!BTProcessLock) { - Log.trace(F("Creating BLE buffer" CR)); - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = advertisedDevice->getAddress().toString(); - BLEdata["mac_type"] = advertisedDevice->getAddress().getType(); - BLEdata["adv_type"] = advertisedDevice->getAdvType(); - Log.notice(F("BT Device detected: %s" CR), BLEdata["id"].as()); - BLEdevice* device = getDeviceByMac(BLEdata["id"].as()); - - if (BTConfig.filterConnectable && device->connect) { - Log.notice(F("Filtered connectable device" CR)); - delete (advertisedDevice); - continue; - } - - if (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(device)) && !isBlack(device))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC) - if (advertisedDevice->haveName()) - BLEdata["name"] = (char*)advertisedDevice->getName().c_str(); - if (advertisedDevice->haveManufacturerData()) { - BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString((uint8_t*)advertisedDevice->getManufacturerData().data(), - advertisedDevice->getManufacturerData().length()); - } - BLEdata["rssi"] = (int)advertisedDevice->getRSSI(); - if (advertisedDevice->haveTXPower()) - BLEdata["txpower"] = (int8_t)advertisedDevice->getTXPower(); - if (BTConfig.presenceEnable) { - hass_presence(BLEdata); // with either only sensors or not we can use it for home assistant room presence component - } - if (advertisedDevice->haveServiceData()) { - int serviceDataCount = advertisedDevice->getServiceDataCount(); - Log.trace(F("Get services data number: %d" CR), serviceDataCount); - for (int j = 0; j < serviceDataCount; j++) { - StaticJsonDocument BLEdataBufferTemp; - JsonObject BLEdataTemp = BLEdataBufferTemp.to(); - BLEdataBufferTemp = BLEdataBuffer; - std::string service_data = convertServiceData(advertisedDevice->getServiceData(j)); - Log.trace(F("Service data: %s" CR), service_data.c_str()); - std::string serviceDatauuid = advertisedDevice->getServiceDataUUID(j).toString(); - Log.trace(F("Service data UUID: %s" CR), (char*)serviceDatauuid.c_str()); - BLEdataTemp["servicedata"] = (char*)service_data.c_str(); - BLEdataTemp["servicedatauuid"] = (char*)serviceDatauuid.c_str(); - PublishDeviceData(BLEdataTemp); - } - } else { - PublishDeviceData(BLEdata); - } - } else { - Log.trace(F("Filtered MAC device" CR)); - } - updateDevicesStatus(); - } - delete (advertisedDevice); - vTaskDelay(10); - } -} - -/** - * BLEscan used to retrieve BLE advertized data from devices without connection - */ -void BLEscan() { - // Don't start the next scan until processing of previous results is complete. - while (uxQueueMessagesWaiting(BLEQueue) || queueLength != 0) { // the criteria on queueLength could be adjusted to parallelize the scan and the queue processing - delay(1); // Wait for queue to empty, a yield here instead of the delay cause the WDT to trigger - } - Log.notice(F("Scan begin" CR)); - BLEScan* pBLEScan = BLEDevice::getScan(); - pBLEScan->setScanCallbacks(&scanCallbacks); - if ((millis() > (timeBetweenActive + BTConfig.intervalActiveScan) || BTConfig.intervalActiveScan == BTConfig.BLEinterval) && !BTConfig.forcePassiveScan) { - pBLEScan->setActiveScan(true); - timeBetweenActive = millis(); - } else { - pBLEScan->setActiveScan(false); - } - pBLEScan->setInterval(BLEScanInterval); - pBLEScan->setWindow(BLEScanWindow); - NimBLEScanResults foundDevices = pBLEScan->getResults(BTConfig.scanDuration, false); - if (foundDevices.getCount()) - scanCount++; - Log.notice(F("Found %d devices, scan number %d end" CR), foundDevices.getCount(), scanCount); - Log.trace(F("Process BLE stack free: %u" CR), uxTaskGetStackHighWaterMark(xProcBLETaskHandle)); -} - -/** - * Connect to BLE devices and initiate the callbacks with a service/characteristic request - */ -# if BLEDecoder -void BLEconnect() { - if (!BTProcessLock) { - Log.notice(F("BLE Connect begin" CR)); - do { - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - BLEdevice* p = *it; - if (p->connect) { - Log.trace(F("Model to connect found: %s" CR), p->macAdr); - NimBLEAddress addr((const char*)p->macAdr, p->macType); - if (p->sensorModel_id == BLEconectable::id::LYWSD03MMC || - p->sensorModel_id == BLEconectable::id::MHO_C401) { - LYWSD03MMC_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - BLEclient.publishData(); - } else if (p->sensorModel_id == BLEconectable::id::DT24_BLE) { - DT24_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - BLEclient.publishData(); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { - BM2_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); - BLEclient.publishData(); - } else if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { - XMWSDJ04MMC_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - BLEclient.publishData(); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1) { - SBS1_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT) { - SBBT_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU) { - SBCU_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - } else { - GENERIC_connect BLEclient(addr); - if (BLEclient.processActions(BLEactions)) { - // If we don't regularly connect to this, disable connections so advertisements - // won't be filtered if BLE_FILTER_CONNECTABLE is set. - p->connect = false; - } - } - if (BLEactions.size() > 0) { - std::vector swap; - for (auto& it : BLEactions) { - if (!it.complete && --it.ttl) { - swap.push_back(it); - } else if (it.addr == NimBLEAddress(p->macAdr, p->macType)) { - if (p->sensorModel_id != BLEconectable::id::DT24_BLE && - 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 != BLEconectable::id::MHO_C401 && - p->sensorModel_id != BLEconectable::id::XMWSDJ04MMC) { - // if irregulary connected to and connection failed clear the connect flag. - p->connect = false; - } - } - } - std::swap(BLEactions, swap); - } - } - } - } while (BLEactions.size() > 0); - Log.notice(F("BLE Connect end" CR)); - } -} -# else -void BLEconnect() {} -# endif - -void stopProcessing(bool deinit) { - if (BTConfig.enabled) { - BTProcessLock = true; - // We stop the scan - Log.notice(F("Stopping BLE scan" CR)); - BLEScan* pBLEScan = BLEDevice::getScan(); - if (pBLEScan->isScanning()) { - pBLEScan->stop(); - } - - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { - Log.notice(F("Stopping BLE tasks" CR)); - //Suspending, deleting tasks and stopping BT to free memory - vTaskSuspend(xCoreTaskHandle); - vTaskDelete(xCoreTaskHandle); - vTaskSuspend(xProcBLETaskHandle); - vTaskDelete(xProcBLETaskHandle); - xSemaphoreGive(semaphoreBLEOperation); - } - // Using deinit to free memory, should only be used if we are going to restart the gateway - if (deinit) - BLEDevice::deinit(true); - } - Log.notice(F("BLE gateway stopped, free heap: %d" CR), ESP.getFreeHeap()); -} - -void coreTask(void* pvParameters) { - while (true) { - if (!BTProcessLock) { - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(30000)) == pdTRUE) { - BLEscan(); - // Launching a connect every TimeBtwConnect - if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { - timeBetweenConnect = millis(); - BLEconnect(); - } - //dumpDevices(); - Log.trace(F("CoreTask stack free: %u" CR), uxTaskGetStackHighWaterMark(xCoreTaskHandle)); - xSemaphoreGive(semaphoreBLEOperation); - } else { - Log.error(F("Failed to start scan - BLE busy" CR)); - } - if (SYSConfig.powerMode > 0) { - int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); // is this enough, it will wait the full deepsleep... - if (scan == 1) BTforceScan(); - ready_to_sleep = true; - } else { - for (int interval = BTConfig.BLEinterval, waitms; interval > 0; interval -= waitms) { - int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); - if (scan == 1) BTforceScan(); // should we break after this? - delay(waitms = interval > 100 ? 100 : interval); // 100ms - } - } - } - delay(1); - } -} - -void setupBTTasksAndBLE() { -# ifdef CONFIG_BTDM_BLE_SCAN_DUPL - BLEDevice::setScanDuplicateCacheSize(BLEScanDuplicateCacheSize); -# endif - BLEDevice::init(""); - xTaskCreateUniversal( - procBLETask, /* Function to implement the task */ - "procBLETask", /* Name of the task */ -# if defined(USE_ESP_IDF) || defined(USE_BLUFI) - 14500, -# else - 9500, /* Stack size in bytes */ -# endif - NULL, /* Task input parameter */ - 2, /* Priority of the task (set higher than core task) */ - &xProcBLETaskHandle, /* Task handle. */ - 1); /* Core where the task should run */ - - // we setup a task with priority one to avoid conflict with other gateways - xTaskCreateUniversal( - coreTask, /* Function to implement the task */ - "coreTask", /* Name of the task */ - 5120, /* Stack size in bytes */ - NULL, /* Task input parameter */ - 1, /* Priority of the task */ - &xCoreTaskHandle, /* Task handle. */ - taskCore); /* Core where the task should run */ -} - -void setupBT() { - BTConfig_init(); - BTConfig_load(); - Log.notice(F("BLE scans interval: %d" CR), BTConfig.BLEinterval); - Log.notice(F("BLE connects interval: %d" CR), BTConfig.intervalConnect); - Log.notice(F("BLE scan duration: %d" CR), BTConfig.scanDuration); - Log.notice(F("Publishing only BLE sensors: %T" CR), BTConfig.pubOnlySensors); - Log.notice(F("Publishing random MAC devices: %T" CR), BTConfig.pubRandomMACs); - Log.notice(F("Adaptive BLE scan: %T" CR), BTConfig.adaptiveScan); - Log.notice(F("Active BLE scan interval: %d" CR), BTConfig.intervalActiveScan); - Log.notice(F("minrssi: %d" CR), -abs(BTConfig.minRssi)); - Log.notice(F("Presence Away Timer: %d" CR), BTConfig.presenceAwayTimer); - Log.notice(F("Moving Timer: %d" CR), BTConfig.movingTimer); - Log.notice(F("Force passive scan: %T" CR), BTConfig.forcePassiveScan); - Log.notice(F("Enabled BLE: %T" CR), BTConfig.enabled); - - atomic_init(&forceBTScan, 0); // in theory, we don't need this - - semaphoreCreateOrUpdateDevice = xSemaphoreCreateBinary(); - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - - semaphoreBLEOperation = xSemaphoreCreateBinary(); - xSemaphoreGive(semaphoreBLEOperation); - - BLEQueue = xQueueCreate(QueueSize, sizeof(NimBLEAdvertisedDevice*)); - if (BTConfig.enabled) { - setupBTTasksAndBLE(); - Log.notice(F("gatewayBT multicore ESP32 setup done" CR)); - } else { - Log.notice(F("gatewayBT multicore ESP32 setup disabled" CR)); - } -} - -boolean valid_service_data(const char* data, int size) { - for (int i = 0; i < size; ++i) { - if (data[i] != 48) // 48 correspond to 0 in ASCII table - return true; - } - return false; -} - -# if defined(ZmqttDiscovery) && BLEDecoder == true -// This function always should be called from the main core as it generates direct mqtt messages -// When overrideDiscovery=true, we publish discovery messages of known devices (even if no new) -void launchBTDiscovery(bool overrideDiscovery) { - if (!overrideDiscovery && newDevices == 0) - return; - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdFALSE) { - Log.error(F("Semaphore NOT taken" CR)); - return; - } - newDevices = 0; - vector localDevices = devices; - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - for (vector::iterator it = localDevices.begin(); it != localDevices.end(); ++it) { - BLEdevice* p = *it; - Log.trace(F("Device mac %s" CR), p->macAdr); - // Do not launch discovery for the devices already discovered (unless we have overrideDiscovery) or that are not unique by their MAC Address (iBeacon, GAEN and Microsoft CDP) - if (overrideDiscovery || !isDiscovered(p)) { - String macWOdots = String(p->macAdr); - macWOdots.replace(":", ""); - if (p->sensorModel_id >= 0) { - Log.trace(F("Looking for Model_id: %d" CR), p->sensorModel_id); - std::string properties = decoder.getTheengProperties(p->sensorModel_id); - Log.trace(F("properties: %s" CR), properties.c_str()); - std::string brand = decoder.getTheengAttribute(p->sensorModel_id, "brand"); - std::string model = decoder.getTheengAttribute(p->sensorModel_id, "model"); - if (displayDeviceName || ForceDeviceName) { - if (p->name[0] != '\0') { - model = p->name; - } - } - std::string model_id = decoder.getTheengAttribute(p->sensorModel_id, "model_id"); - - // Check for tracker status - bool isTracker = false; - std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); - if (tag.length() >= 4) { - isTracker = checkIfIsTracker(tag[3]); - } - - String discovery_topic = String(subjectBTtoMQTT) + "/" + macWOdots; - 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 - if (isTracker) { - String tracker_name = String(model_id.c_str()) + "-tracker"; - String tracker_id = macWOdots + "-tracker"; - createDiscovery("device_tracker", - discovery_topic.c_str(), tracker_name.c_str(), 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 == TheengsDecoder::BLE_ID_NUM::BC08) { - String sensor_name = String(model_id.c_str()) + "-moving"; - String sensor_id = macWOdots + "-moving"; - createDiscovery("binary_sensor", - discovery_topic.c_str(), sensor_name.c_str(), sensor_id.c_str(), - will_Topic, "moving", "{% if value_json.get('accx') -%}on{%- else -%}off{%- endif %}", - "on", "off", "", - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } - if (!properties.empty()) { - StaticJsonDocument jsonBuffer; - auto error = deserializeJson(jsonBuffer, properties); - if (error) { - if (jsonBuffer.overflowed()) { - // This should not happen if JSON_MSG_BUFFER is large enough for - // the Theengs json properties - Log.error(F("JSON deserialization of Theengs properties overflowed (error %s), buffer capacity: %u. Program might crash. Properties json: %s" CR), - error.c_str(), jsonBuffer.capacity(), properties.c_str()); - } else { - Log.error(F("JSON deserialization of Theengs properties errored: %" CR), - error.c_str()); - } - } - for (JsonPair prop : jsonBuffer["properties"].as()) { - Log.trace(F("Key: %s"), prop.key().c_str()); - Log.trace(F("Unit: %s"), prop.value()["unit"].as()); - Log.trace(F("Name: %s"), prop.value()["name"].as()); - String entity_name = ""; - if (displayDeviceName || ForceDeviceName) { - entity_name = String(model.c_str()) + "-" + String(prop.key().c_str()); - } else { - entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); - } - String unique_id = macWOdots + "-" + String(prop.key().c_str()); - String value_template = "{{ value_json." + String(prop.key().c_str()) + " | is_defined }}"; - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1 && strcmp(prop.key().c_str(), "state") == 0) { - String payload_on = "{\"model_id\":\"X1\",\"cmd\":\"on\",\"id\":\"" + String(p->macAdr) + "\"}"; - String payload_off = "{\"model_id\":\"X1\",\"cmd\":\"off\",\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("switch", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "switch", value_template.c_str(), - payload_on.c_str(), payload_off.c_str(), "", 0, - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone, "off", "on"); - unique_id = macWOdots + "-press"; - entity_name = String(model_id.c_str()) + "-press"; - String payload_press = "{\"model_id\":\"X1\",\"cmd\":\"press\",\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("button", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "button", "", - payload_press.c_str(), "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT && strcmp(prop.key().c_str(), "open") == 0) { - value_template = "{% if value_json.direction == \"up\" -%} {{ 100 - value_json.open/2 }}{% elif value_json.direction == \"down\" %}{{ value_json.open/2 }}{% else %} {{ value_json.open/2 }}{%- endif %}"; - String command_template = "{\"model_id\":\"W270160X\",\"tilt\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("cover", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "cover", value_template.c_str(), - "50", "", "", 0, - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - "blind", nullptr, nullptr, nullptr, command_template.c_str()); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU && strcmp(prop.key().c_str(), "position") == 0) { - String command_template = "{\"model_id\":\"W070160X\",\"position\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("cover", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "cover", "{{ value_json.position }}", - "0", "100", "", 0, - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - "curtain", nullptr, nullptr, nullptr, command_template.c_str()); - } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && - strcmp(prop.key().c_str(), "weighing_mode") == 0) { - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "enum", value_template.c_str(), - "", "", prop.value()["unit"], - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassMeasurement, nullptr, nullptr, "[\"person\",\"object\"]"); - } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && - strcmp(prop.key().c_str(), "unit") == 0) { - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "enum", value_template.c_str(), - "", "", prop.value()["unit"], - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassMeasurement, nullptr, nullptr, "[\"lb\",\"kg\",\"jin\"]"); - } else if (strcmp(prop.value()["unit"], "string") == 0 && strcmp(prop.key().c_str(), "mac") != 0) { - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "", "", "", - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (p->sensorModel_id == TheengsDecoder::MUE4094RT && strcmp(prop.value()["unit"], "status") == 0) { // This device does not a broadcast when there is nothing detected so adding a timeout - createDiscovery("binary_sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "True", "False", "", - BTConfig.presenceAwayTimer / 1000, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (strcmp(prop.value()["unit"], "status") == 0) { - createDiscovery("binary_sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "True", "False", "", - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (strcmp(prop.key().c_str(), "device") != 0 && strcmp(prop.key().c_str(), "mac") != 0) { // Exception on device and mac as these ones are not sensors - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "", "", prop.value()["unit"], - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassMeasurement); - } - } - } - } 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) { - // 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"); - } - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { - // Sensor discovery - BM2Discovery(macWOdots.c_str(), "BM2"); - // Device tracker discovery - String tracker_id = macWOdots + "-tracker"; - createDiscovery("device_tracker", - discovery_topic.c_str(), "BM2-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"); - } - if (p->sensorModel_id == BLEconectable::id::MHO_C401) { - MHO_C401Discovery(macWOdots.c_str(), "MHO-C401"); - } - if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { - XMWSDJ04MMCDiscovery(macWOdots.c_str(), "XMWSDJ04MMC"); - } - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC) { - HHCCJCY01HHCCDiscovery(macWOdots.c_str(), "HHCCJCY01HHCC"); - } - } else { - Log.trace(F("Device UNKNOWN_MODEL %s" CR), p->macAdr); - } - } - } - p->isDisc = true; // we don't need the semaphore and all the search magic via createOrUpdateDevice - } else { - Log.trace(F("Device already discovered or that doesn't require discovery %s" CR), p->macAdr); - } - } -} -# else -void launchBTDiscovery(bool overrideDiscovery) {} -# endif - -# if BLEDecryptor -// ** TODO - Hex string to bytes, there is probably a function for this already just need to find it -int hexToBytes(String hex, uint8_t *out, size_t maxLen) { - int len = hex.length(); - if (len % 2 || len / 2 > maxLen) return -1; - for (int i = 0, j = 0; i < len; i += 2, j++) { - out[j] = (uint8_t) strtol(hex.substring(i, i + 2).c_str(), nullptr, 16); - } - return len / 2; -} -// Reverse bytes -void reverseBytes(uint8_t *data, size_t length) { - size_t i; - for (i = 0; i < length / 2; i++) { - uint8_t temp = data[i]; - data[i] = data[length - 1 - i]; - data[length - 1 - i] = temp; - } -} -# endif - -# if BLEDecoder -void process_bledata(JsonObject& BLEdata) { - yield(); // Necessary to let the loop run in case of connectivity issues - if (!BLEdata.containsKey("id")) { - Log.error(F("No mac address in the payload" CR)); - return; - } - const char* mac = BLEdata["id"].as(); - Log.trace(F("Processing BLE data %s" CR), BLEdata["id"].as()); - int model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); - int mac_type = BLEdata["mac_type"].as(); - -# if BLEDecryptor - if (BLEdata["encr"] && (BLEdata["encr"].as() >0 && BLEdata["encr"].as() <=2)) { - // Decrypting Encrypted BLE Data PVVX, BTHome or Victron - Log.trace(F("[BLEDecryptor] Decrypt ENCR:%d ModelID:%s Payload:%s" CR), BLEdata["encr"].as(), BLEdata["model_id"].as(), BLEdata["cipher"].as()); - - // MAC address - String macWOdots = BLEdata["id"].as(); // Mac Address without dots - macWOdots.replace(":", ""); - unsigned char macAddress[6]; - int maclen = hexToBytes(macWOdots, macAddress, 6); - if (maclen != 6) { - Log.error(F("[BLEDecryptor] Invalid MAC Address length %d" CR), maclen); - return; - } - - // AES decryption key - unsigned char bleaeskey[16]; - int bleaeskeylength = 0; - if (ble_aes_keys.containsKey(macWOdots)){ - Log.trace(F("[BLEDecryptor] Custom AES key %s" CR), ble_aes_keys[macWOdots].as()); - bleaeskeylength = hexToBytes(ble_aes_keys[macWOdots], bleaeskey, 16); - } else { - Log.trace(F("[BLEDecryptor] Default AES key" CR)); - bleaeskeylength = hexToBytes(ble_aes, bleaeskey, 16); - } - // Check AES Key - if (bleaeskeylength != 16) { - Log.error(F("[BLEDecryptor] Invalid key length %d" CR), bleaeskeylength); - return; - } - - // Build nonce and aad - uint8_t nonce[16]; - int noncelength = 0; - unsigned char aad[1]; - int aadLength; - - if (BLEdata["encr"].as() == 1){ // PVVX Encrypted - noncelength = 11; // 11 bytes - reverseBytes(macAddress, 6); // 6 bytes: device address in reverse - memcpy(nonce, macAddress, 6); - int maclen = hexToBytes(macWOdots, macAddress, 6); - - unsigned char servicedata[16]; - int servicedatalen = hexToBytes(BLEdata["servicedata"].as(), servicedata, 16); - nonce[6] = servicedatalen + 3; // 1 byte : length of (service data + type and UUID) - nonce[7] = 0x16; // 1 byte : "16" -> AD type for "Service Data - 16-bit UUID" - nonce[8] = 0x1A; // 2 bytes: "1a18" -> UUID 181a in little-endian - nonce[9] = 0x18; // - unsigned char ctr[1]; // 1 byte : counter - int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 1); - if (ctrlen != 1) { - Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); - return; - } - nonce[10] = ctr[0]; - aad[0] = 0x11; - aadLength = 1; - Log.trace(F("[BLEDecryptor] PVVX nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); - - } else if (BLEdata["encr"].as() == 2){ // BTHome V2 Encrypted - noncelength = 13; // 13 bytes - memcpy(nonce, macAddress, 6); - nonce[6] = 0xD2; // UUID - nonce[7] = 0xFC; - nonce[8] = 0x41; // BTHome Device Data encrypted payload byte - unsigned char ctr[4]; // Counter - int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 4); - if (ctrlen != 4) { - Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); - return; - } - memcpy(&nonce[9], ctr, 4); - aad[0] = 0x00; - aadLength = 0; - Log.trace(F("[BLEDecryptor] BTHomeV2 nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); - - } else if (BLEdata["encr"].as() == 3){ - nonce[16] = {0}; // Victron has a 16 byte zero padded nonce with IV bytes 6,7 - unsigned char iv[2]; - int ivlen = hexToBytes(BLEdata["ctr"].as(), iv, 2); - if (ivlen != 2) { - Log.error(F("[BLEDecryptor] Invalid iv length %d" CR), ivlen); - return; - } - memcpy(nonce, iv, 2); - Log.trace(F("[BLEDecryptor] Victron nonce %s" CR), NimBLEUtils::dataToHexString(nonce, 16).c_str()); - } else { - return; // No match - } - - // Ciphertext to bytes - int cipherlen = sizeof(BLEdata["cipher"].as()); - unsigned char ciphertext[cipherlen]; - int ciphertextlen = hexToBytes(BLEdata["cipher"].as(), ciphertext, cipherlen); - unsigned char decrypted[ciphertextlen]; // Decrypted payload - - // Decrypt ciphertext - if (BLEdata["encr"].as() == 1 || BLEdata["encr"].as() == 2) { - // Decrypt PVVX and BTHome V2 ciphertext using AES CCM - mbedtls_ccm_context ctx; - mbedtls_ccm_init(&ctx); - if (mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, bleaeskey, 128) != 0) { - Log.error(F("[BLEDecryptor] Failed to set AES key to mbedtls" CR)); - return; - } - - // Message Integrity Check (MIC) - unsigned char mic[4]; - int miclen = hexToBytes(BLEdata["mic"].as(), mic, 4); - if (miclen != 4) { - Log.error(F("[BLEDecryptor] Invalid MIC length %d" CR), miclen); - return; - } - - int ret = mbedtls_ccm_auth_decrypt( - &ctx, // AES Key - ciphertextlen, // length of ciphertext - nonce, noncelength, // Nonce - aad, aadLength, // AAD - ciphertext, // input ciphertext - decrypted, // output plaintext - mic, sizeof(mic) // Message Integrity Check - ); - mbedtls_ccm_free(&ctx); - - if (ret == 0) { - Log.notice(F("[BLEDecryptor] Decryption successful" CR)); - } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { - Log.error(F("[BLEDecryptor] Authentication failed." CR)); - return; - } else { - Log.error(F("[BLEDecryptor] Decryption failed with error: %X" CR), ret); - return; - } - - // Build new servicedata - if (BLEdata["encr"].as() == 1){ // PVVX - BLEdata["servicedata"] = NimBLEUtils::dataToHexString(decrypted, ciphertextlen); - } else if (BLEdata["encr"].as() == 2) { // BTHomeV2 - // Build new servicedata - uint8_t newservicedata[3 + ciphertextlen]; - newservicedata[0] = 0x40; // Decrypted BTHomeV2 Packet Type - newservicedata[1] = 0x00; // Packet counter which the PVVX BTHome non-encrypted has but the encrypted does not - newservicedata[2] = 0x00; // **TODO Convert the ctr to the packet counter or just stick with 0? - memcpy(&newservicedata[3], decrypted, ciphertextlen); - BLEdata["servicedata"] = NimBLEUtils::dataToHexString(newservicedata, ciphertextlen + 3); - } else { - return; - } - Log.trace(F("[BLEDecryptor] Decrypted servicedata %s" CR), BLEdata["servicedata"].as()); - - } else if (BLEdata["encr"].as() == 3) { - // Decrypt Victron Energy encrypted advertisements. - size_t nc_off = 0; - uint8_t stream_block[16] = {0}; - - mbedtls_aes_context ctx; - mbedtls_aes_init(&ctx); - mbedtls_aes_setkey_enc(&ctx, bleaeskey, 128); - int ret = mbedtls_aes_crypt_ctr( - &ctx, // AES Key - ciphertextlen, // length of ciphertext - &nc_off, - nonce, // 16 byte nonce with 2 bytes iv - stream_block, - ciphertext, // input ciphertext - decrypted // output plaintext - ); - mbedtls_aes_free(&ctx); - - if (ret == 0) { - Log.notice(F("[BLEDecryptor] Victron Decryption successful" CR)); - } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { - Log.error(F("[BLEDecryptor] Victron Authentication failed." CR)); - return; - } else { - Log.error(F("[BLEDecryptor] Victron decryption failed with error: %X" CR), ret); - return; - } - - // Build new manufacturerdata - unsigned char manufacturerdata[10 + ciphertextlen]; - int manufacturerdatalen = hexToBytes(BLEdata["manufacturerdata"].as(), manufacturerdata, 10); - manufacturerdata[2] = 0x11; // Replace byte 2 with "11" indicate decrypted data - manufacturerdata[7] = 0xff; // Replace byte 7 with "ff" to indicate decrypted data - manufacturerdata[8] = 0xff; // Replace byte 8 with "ff" to indicate decrypted data - memcpy(&manufacturerdata[8], decrypted, ciphertextlen); // Append the decrypted payload to the manufacturer data - BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString(manufacturerdata, 10 + ciphertextlen); // Rebuild manufacturerdata - Log.trace(F("[BLEDecryptor] Victron decrypted manufacturerdata %s" CR), BLEdata["manufacturerdata"].as()); - } - - // Print before and after decoder post decryption - // serializeJsonPretty(BLEdata, Serial); - model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); - // serializeJsonPretty(BLEdata, Serial); - Log.trace(F("[BLEDecryptor] Decrypted model_id %d" CR), model_id); - - // Remove the cipher fields from BLEdata - BLEdata.remove("encr"); - BLEdata.remove("cipher"); - BLEdata.remove("ctr"); - BLEdata.remove("mic"); - - } -# endif - - // Convert prmacs to RMACS until or if OMG gets Identity MAC/IRK decoding - if (BLEdata["prmac"]) { - BLEdata.remove("prmac"); - if (BLEdata["track"]) { - BLEdata.remove("track"); - } - BLEdata["type"] = "RMAC"; - Log.trace(F("Potential RMAC (prmac) converted to RMAC" CR)); - } - const char* deviceName = BLEdata["name"] | ""; - - 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 - 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 - createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); - } else { - createOrUpdateDevice(mac, device_flags_init, model_id, mac_type, deviceName); - if (BTConfig.adaptiveScan == true && (BTConfig.BLEinterval != MinTimeBtwScan || BTConfig.intervalActiveScan != MinTimeBtwScan)) { - if (BLEdata.containsKey("acts") && BLEdata.containsKey("cont")) { - if (BLEdata["acts"] && BLEdata["cont"]) { - BTConfig.BLEinterval = MinTimeBtwScan; - BTConfig.intervalActiveScan = MinTimeBtwScan; - BTConfig.scanDuration = MinScanDuration; - Log.notice(F("Active and continuous scanning required, parameters adapted" CR)); - stateBTMeasures(false); - } - } else if (BLEdata.containsKey("cont") && BTConfig.BLEinterval != MinTimeBtwScan) { - if (BLEdata["cont"]) { - BTConfig.BLEinterval = MinTimeBtwScan; - if ((BLEdata["type"].as()).compare("CTMO") == 0) { - BTConfig.scanDuration = MinScanDuration; - } - Log.notice(F("Passive continuous scanning required, parameters adapted" CR)); - stateBTMeasures(false); - } - } - } - } - } else { - if (BLEdata.containsKey("name")) { // Connectable only devices - std::string name = BLEdata["name"]; - if (name.compare("LYWSD03MMC") == 0) - model_id = BLEconectable::id::LYWSD03MMC; - else if (name.compare("DT24-BLE") == 0) - model_id = BLEconectable::id::DT24_BLE; - else if (name.compare("MHO-C401") == 0) - model_id = BLEconectable::id::MHO_C401; - else if (name.compare("XMWSDJ04MMC") == 0) - model_id = BLEconectable::id::XMWSDJ04MMC; - - if (model_id > 0) { - Log.trace(F("Connectable device found: %s" CR), name.c_str()); - createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); - } - } else if (BTConfig.extDecoderEnable && model_id < 0 && BLEdata.containsKey("servicedata")) { - const char* service_data = (const char*)(BLEdata["servicedata"] | ""); - if (strstr(service_data, "209800") != NULL) { - model_id = TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC; - Log.trace(F("Connectable device found: HHCCJCY01HHCC" CR)); - createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); - } - } - } - } else { - Log.trace(F("Random MAC or iBeacon device filtered" CR)); - } - if (!BTConfig.extDecoderEnable && model_id < 0) { - Log.trace(F("No eligible device found " CR)); - } -} -void PublishDeviceData(JsonObject& BLEdata) { - if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough - // Decode the payload - process_bledata(BLEdata); - // If the device is a random MAC and pubRandomMACs is false we don't publish this payload - if (!BTConfig.pubRandomMACs && (BLEdata["type"].as()).compare("RMAC") == 0) { - Log.trace(F("Random MAC, device filtered" CR)); - return; - } - // If pubAdvData is false we don't publish the adv data - if (!BTConfig.pubAdvData) { - BLEdata.remove("servicedatauuid"); - BLEdata.remove("servicedata"); - BLEdata.remove("manufacturerdata"); - BLEdata.remove("mac_type"); - BLEdata.remove("adv_type"); - // tag device properties - // BLEdata.remove("type"); type is used by the WebUI module to determine the template used to display the signal - BLEdata.remove("cidc"); - BLEdata.remove("acts"); - BLEdata.remove("cont"); - BLEdata.remove("track"); - BLEdata.remove("ctrl"); - } - // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid - if (BLEdata.containsKey("distance")) { - if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { - BLEdata["mac"] = BLEdata["id"].as(); - BLEdata["id"] = BLEdata["uuid"].as(); - } - String topic = String(mqtt_topic) + BTConfig.presenceTopic + String(gateway_name); - Log.trace(F("Pub HA Presence %s" CR), topic.c_str()); - BLEdata["topic"] = topic; - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } - - // If the device is not a sensor and pubOnlySensors is true we don't publish this payload - if (!BTConfig.pubOnlySensors || BLEdata.containsKey("model") || !BLEDecoder) { // Identified device - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } else { - Log.notice(F("Not a sensor device filtered" CR)); - return; - } - -# if BLEDecoder - if (enableMultiGTWSync && BLEdata.containsKey("model_id") && BLEdata.containsKey("id")) { - // Publish tracker sync message - bool isTracker = false; - std::string tag = decoder.getTheengAttribute(BLEdata["model_id"].as(), "tag"); - if (tag.length() >= 4) { - isTracker = checkIfIsTracker(tag[3]); - } - - if (isTracker) { - StaticJsonDocument BLEdataBuffer; - JsonObject TrackerSyncdata = BLEdataBuffer.to(); - TrackerSyncdata["gatewayid"] = gateway_name; - TrackerSyncdata["trackerid"] = BLEdata["id"].as(); - String topic = String(mqtt_topic) + String(subjectTrackerSync); - TrackerSyncdata["topic"] = topic.c_str(); - enqueueJsonObject(TrackerSyncdata); - } - } -# endif - } else { - Log.notice(F("Low rssi, device filtered" CR)); - return; - } -} -# else -void process_bledata(JsonObject& BLEdata) {} -void PublishDeviceData(JsonObject& BLEdata) { - if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough - // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid - if (BLEdata.containsKey("distance")) { - if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { - BLEdata["mac"] = BLEdata["id"].as(); - BLEdata["id"] = BLEdata["uuid"].as(); - } - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } else { - Log.notice(F("Low rssi, device filtered" CR)); - return; - } -} -# endif - -void hass_presence(JsonObject& HomePresence) { - int BLErssi = HomePresence["rssi"]; - Log.trace(F("BLErssi %d" CR), BLErssi); - int txPower = HomePresence["txpower"] | 0; - if (txPower >= 0) - txPower = -59; //if tx power is not found we set a default calibration value - Log.trace(F("TxPower: %d" CR), txPower); - double ratio = BLErssi * 1.0 / txPower; - double distance; - if (ratio < 1.0) { - distance = pow(ratio, 10); - } else { - distance = (0.89976) * pow(ratio, 7.7095) + 0.111; - } - HomePresence["distance"] = distance; - Log.trace(F("Ble distance %D" CR), distance); -} - -void BTforceScan() { - if (!BTProcessLock) { - BLEscan(); - Log.trace(F("Scan done" CR)); - if (BTConfig.bleConnect) - BLEconnect(); - } else { - Log.trace(F("Cannot launch scan due to other process running" CR)); - } -} - -void immediateBTAction(void* pvParameters) { - if (BLEactions.size()) { - // Immediate action; we need to prevent the normal connection action and stop scanning - BTProcessLock = true; - NimBLEScan* pScan = NimBLEDevice::getScan(); - if (pScan->isScanning()) { - pScan->stop(); - } - - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { - // swap the vectors so only this device is processed - std::vector dev_swap; - dev_swap.push_back(getDeviceByMac(BLEactions.back().addr.toString().c_str())); - std::swap(devices, dev_swap); - - std::vector act_swap; - act_swap.push_back(BLEactions.back()); - BLEactions.pop_back(); - std::swap(BLEactions, act_swap); - - // Unlock here to allow the action to be performed - BTProcessLock = false; - BLEconnect(); - // back to normal - std::swap(devices, dev_swap); - std::swap(BLEactions, act_swap); - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - } else { - Log.error(F("CreateOrUpdate Semaphore NOT taken" CR)); - } - - // If we stopped the scheduled connect for this action, do the scheduled now - if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { - timeBetweenConnect = millis(); - BLEconnect(); - } - xSemaphoreGive(semaphoreBLEOperation); - } else { - Log.error(F("BLE busy - immediateBTAction not sent" CR)); - gatewayState = GatewayState::ERROR; - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = BLEactions.back().addr.toString(); - BLEdata["success"] = false; - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - BLEactions.pop_back(); - BTProcessLock = false; - } - } - vTaskDelete(NULL); -} - -void startBTActionTask() { - TaskHandle_t th; - xTaskCreateUniversal( - immediateBTAction, /* Function to implement the task */ - "imActTask", /* Name of the task */ - 8000, /* Stack size in bytes */ - NULL, /* Task input parameter */ - 3, /* Priority of the task (set higher than core task) */ - &th, /* Task handle. */ - 1); /* Core where the task should run */ -} - -# if BLEDecoder -void KnownBTActions(JsonObject& BTdata) { - if (!BTdata.containsKey("id")) { - Log.error(F("BLE mac address missing" CR)); - gatewayState = GatewayState::ERROR; - return; - } - - BLEAction action{}; - action.write = true; - action.ttl = 3; - bool res = false; - if (BTdata.containsKey("model_id") && BTdata["model_id"].is()) { - if (BTdata["model_id"] == "X1") { - if (BTdata.containsKey("cmd") && BTdata["cmd"].is()) { - action.value_type = BLE_VAL_STRING; - std::string val = BTdata["cmd"].as(); // Fix #1694 - action.value = val; - createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, - TheengsDecoder::BLE_ID_NUM::SBS1, 1); - res = true; - } - } else if (BTdata["model_id"] == "W270160X") { - if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { - action.value_type = BLE_VAL_INT; - res = true; - } else if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { - action.value_type = BLE_VAL_STRING; - res = true; - } - if (res) { - std::string val = BTdata["tilt"].as(); // Fix #1694 - action.value = val; - createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, - TheengsDecoder::BLE_ID_NUM::SBBT, 1); - } - } else if (BTdata["model_id"] == "W070160X") { - if (BTdata.containsKey("position") && BTdata["position"].is()) { - action.value_type = BLE_VAL_INT; - res = true; - } else if (BTdata.containsKey("position") && BTdata["position"].is()) { - action.value_type = BLE_VAL_STRING; - res = true; - } - if (res) { - std::string val = BTdata["position"].as(); // Fix #1694 - action.value = val; - createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, - TheengsDecoder::BLE_ID_NUM::SBCU, 1); - } - } - if (res) { - action.addr = NimBLEAddress(BTdata["id"].as(), 1); - BLEactions.push_back(action); - startBTActionTask(); - } else { - Log.error(F("BLE action not recognized" CR)); - gatewayState = GatewayState::ERROR; - } - } -} -# else -void KnownBTActions(JsonObject& BTdata) {} -# endif - -void XtoBTAction(JsonObject& BTdata) { - BLEAction action{}; - action.ttl = BTdata.containsKey("ttl") ? (uint8_t)BTdata["ttl"] : 1; - action.value_type = BLE_VAL_STRING; - if (BTdata.containsKey("value_type")) { - String vt = BTdata["value_type"]; - vt.toUpperCase(); - if (vt == "HEX") - action.value_type = BLE_VAL_HEX; - else if (vt == "INT") - action.value_type = BLE_VAL_INT; - else if (vt == "FLOAT") - action.value_type = BLE_VAL_FLOAT; - else if (vt != "STRING") { - Log.error(F("BLE value type invalid %s" CR), vt.c_str()); - return; - } - } - - Log.trace(F("BLE ACTION TTL = %u" CR), action.ttl); - action.complete = false; - if (BTdata.containsKey("ble_write_address") && - BTdata.containsKey("ble_write_service") && - BTdata.containsKey("ble_write_char") && - BTdata.containsKey("ble_write_value")) { - action.addr = NimBLEAddress(BTdata["ble_write_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); - action.service = NimBLEUUID((const char*)BTdata["ble_write_service"]); - action.characteristic = NimBLEUUID((const char*)BTdata["ble_write_char"]); - std::string val = BTdata["ble_write_value"].as(); // Fix #1694 - action.value = val; - action.write = true; - Log.trace(F("BLE ACTION Write" CR)); - } else if (BTdata.containsKey("ble_read_address") && - BTdata.containsKey("ble_read_service") && - BTdata.containsKey("ble_read_char")) { - action.addr = NimBLEAddress(BTdata["ble_read_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); - action.service = NimBLEUUID((const char*)BTdata["ble_read_service"]); - action.characteristic = NimBLEUUID((const char*)BTdata["ble_read_char"]); - action.write = false; - Log.trace(F("BLE ACTION Read" CR)); - } else { - return; - } - - createOrUpdateDevice(action.addr.toString().c_str(), device_flags_connect, UNKWNON_MODEL, action.addr.getType()); - - BLEactions.push_back(action); - if (BTdata.containsKey("immediate") && BTdata["immediate"].as()) { - startBTActionTask(); - } -} - -void XtoBT(const char* topicOri, JsonObject& BTdata) { // json object decoding - if (cmpToMainTopic(topicOri, subjectMQTTtoBTset)) { - Log.trace(F("MQTTtoBT json set" CR)); - - // Black list & white list set - bool WorBupdated; - WorBupdated = updateWorB(BTdata, true); - WorBupdated |= updateWorB(BTdata, false); - - if (WorBupdated) { - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { - //dumpDevices(); - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - } - } - - // Force scan now - if (BTdata.containsKey("interval") && BTdata["interval"] == 0) { - Log.notice(F("BLE forced scan" CR)); - atomic_store_explicit(&forceBTScan, 1, ::memory_order_seq_cst); // ask the other core to do the scan for us - } - - /* - * Configuration modifications priorities: - * First `init=true` and `load=true` commands are executed (if both are present, INIT prevails on LOAD) - * Then parameters included in json are taken in account - * Finally `erase=true` and `save=true` commands are executed (if both are present, ERASE prevails on SAVE) - */ - if (BTdata.containsKey("init") && BTdata["init"].as()) { - // Restore the default (initial) configuration - BTConfig_init(); - } else if (BTdata.containsKey("load") && BTdata["load"].as()) { - // Load the saved configuration, if not initialised - BTConfig_load(); - } - - // Load config from json if available - BTConfig_fromJson(BTdata); - - } else if (cmpToMainTopic(topicOri, subjectMQTTtoBT)) { - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { - KnownBTActions(BTdata); - XtoBTAction(BTdata); - xSemaphoreGive(semaphoreBLEOperation); - } else { - Log.error(F("BLE busy - BTActions not sent" CR)); - gatewayState = GatewayState::ERROR; - } - } else if (strstr(topicOri, subjectTrackerSync) != NULL) { - if (BTdata.containsKey("gatewayid") && BTdata.containsKey("trackerid") && BTdata["gatewayid"] != gateway_name) { - BLEdevice* device = getDeviceByMac(BTdata["trackerid"].as()); - if (device != &NO_BT_DEVICE_FOUND && device->lastUpdate != 0) { - device->lastUpdate = 0; - Log.notice(F("Tracker %s disassociated by gateway %s" CR), BTdata["trackerid"].as(), BTdata["gatewayid"].as()); - } - } - } -} -#endif +/* + OpenMQTTGateway - ESP8266 or Arduino program for home automation + + Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker + Send and receiving command by MQTT + + This gateway enables to: + - publish MQTT data to a topic related to BLE devices data + + Copyright: (c)Florian ROBERT + + This file is part of OpenMQTTGateway. + + OpenMQTTGateway is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenMQTTGateway is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Thanks to wolass https://github.com/wolass for suggesting me HM 10 and dinosd https://github.com/dinosd/BLE_PROXIMITY for inspiring me how to implement the gateway +*/ +#include "User_config.h" + +#ifdef ZgatewayBT +# include "TheengsCommon.h" + +SemaphoreHandle_t semaphoreCreateOrUpdateDevice; +SemaphoreHandle_t semaphoreBLEOperation; +QueueHandle_t BLEQueue; +unsigned long scanCount = 0; +# include +# include +# include +# include +# include +# include + +# include + +# include "TheengsCommon.h" +# include "config_mqttDiscovery.h" +# include "gatewayBLEConnect.h" +# include "soc/timer_group_reg.h" +# include "soc/timer_group_struct.h" + +using namespace std; + +// Global struct to store live BT configuration data +BTConfig_s BTConfig; + +# if BLEDecoder +# include +# if BLEDecryptor +# include "mbedtls/ccm.h" +# include "mbedtls/aes.h" +# endif +TheengsDecoder decoder; +# endif + +static TaskHandle_t xCoreTaskHandle; +static TaskHandle_t xProcBLETaskHandle; + +struct decompose { + int start; + int len; + bool reverse; +}; + +vector BLEactions; + +vector devices; +int newDevices = 0; + +static BLEdevice NO_BT_DEVICE_FOUND = { + {0}, + 0, + false, + false, + false, + false, + (char)UNKWNON_MODEL, + 0, +}; +static bool oneWhite = false; + +extern bool BTProcessLock; +extern int queueLength; + +void setupBTTasksAndBLE(); +bool checkIfIsTracker(char ch); +void hass_presence(JsonObject& HomePresence); +void BTforceScan(); + +void BTConfig_init() { + BTConfig.bleConnect = AttemptBLEConnect; + BTConfig.BLEinterval = TimeBtwRead; + BTConfig.adaptiveScan = AdaptiveBLEScan; + BTConfig.intervalActiveScan = TimeBtwActive; + BTConfig.intervalConnect = TimeBtwConnect; + BTConfig.scanDuration = Scan_duration; + BTConfig.pubOnlySensors = PublishOnlySensors; + BTConfig.pubRandomMACs = PublishRandomMACs; + BTConfig.presenceEnable = HassPresence; + BTConfig.presenceTopic = subjectHomePresence; + BTConfig.presenceUseBeaconUuid = useBeaconUuidForPresence; + BTConfig.minRssi = MinimumRSSI; + BTConfig.extDecoderEnable = UseExtDecoder; + BTConfig.extDecoderTopic = MQTTDecodeTopic; + BTConfig.filterConnectable = BLE_FILTER_CONNECTABLE; + BTConfig.pubAdvData = pubBLEAdvData; + BTConfig.pubBeaconUuidForTopic = useBeaconUuidForTopic; + BTConfig.ignoreWBlist = false; + BTConfig.presenceAwayTimer = PresenceAwayTimer; + BTConfig.movingTimer = MovingTimer; + BTConfig.forcePassiveScan = false; + BTConfig.enabled = EnableBT; +} + +unsigned long timeBetweenConnect = 0; +unsigned long timeBetweenActive = 0; + +String stateBTMeasures(bool start) { + StaticJsonDocument jsonBuffer; + JsonObject jo = jsonBuffer.to(); + jo["bleconnect"] = BTConfig.bleConnect; + jo["interval"] = BTConfig.BLEinterval; + jo["adaptivescan"] = BTConfig.adaptiveScan; + jo["intervalacts"] = BTConfig.intervalActiveScan; + jo["intervalcnct"] = BTConfig.intervalConnect; + jo["scanduration"] = BTConfig.scanDuration; + jo["hasspresence"] = BTConfig.presenceEnable; + jo["prestopic"] = BTConfig.presenceTopic; + jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; + jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value + jo["extDecoderEnable"] = BTConfig.extDecoderEnable; + jo["extDecoderTopic"] = BTConfig.extDecoderTopic; + jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; + jo["ignoreWBlist"] = BTConfig.ignoreWBlist; + jo["forcepscn"] = BTConfig.forcePassiveScan; + jo["tskstck"] = uxTaskGetStackHighWaterMark(xProcBLETaskHandle); + jo["crstck"] = uxTaskGetStackHighWaterMark(xCoreTaskHandle); + jo["enabled"] = BTConfig.enabled; + jo["scnct"] = scanCount; +# if BLEDecoder + jo["onlysensors"] = BTConfig.pubOnlySensors; + jo["randommacs"] = BTConfig.pubRandomMACs; + jo["filterConnectable"] = BTConfig.filterConnectable; + jo["pubadvdata"] = BTConfig.pubAdvData; + jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; + jo["movingtimer"] = BTConfig.movingTimer; +# endif + + if (start) { + Log.notice(F("BT sys: ")); + serializeJsonPretty(jsonBuffer, Serial); + Serial.println(); + return ""; // Do not try to erase/write/send config at startup + } + String output; + serializeJson(jo, output); + jo["origin"] = subjectBTtoMQTT; + enqueueJsonObject(jo, QueueSemaphoreTimeOutTask); + return (output); +} + +void BTConfig_fromJson(JsonObject& BTdata, bool startup = false) { + // Attempts to connect to eligible devices or not + Config_update(BTdata, "bleconnect", BTConfig.bleConnect); + // Identify AdaptiveScan deactivation to pass to continuous mode or activation to come back to default settings + if (startup == false) { + if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == false && BTConfig.presenceEnable == true) { + BTdata["adaptivescan"] = true; + } else if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == true && BTConfig.presenceEnable == false) { + BTdata["adaptivescan"] = false; + } + + if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == false && BTConfig.adaptiveScan == true) { + BTdata["interval"] = MinTimeBtwScan; + BTdata["intervalacts"] = MinTimeBtwScan; + BTdata["scanduration"] = MinScanDuration; + } else if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == true && BTConfig.adaptiveScan == false) { + BTdata["interval"] = TimeBtwRead; + BTdata["intervalacts"] = TimeBtwActive; + BTdata["scanduration"] = Scan_duration; + } + // Identify if the gateway is enabled or not and stop start accordingly + if (BTdata.containsKey("enabled") && BTdata["enabled"] == false && BTConfig.enabled == true) { + // Stop the gateway but without deinit to enable a future BT restart + stopProcessing(false); + } else if (BTdata.containsKey("enabled") && BTdata["enabled"] == true && BTConfig.enabled == false) { + BTProcessLock = false; + setupBTTasksAndBLE(); + } + } + // Home Assistant presence message + Config_update(BTdata, "hasspresence", BTConfig.presenceEnable); + // Time before before active scan + // Scan interval set - and avoid intervalacts to be lower than interval + if (BTdata.containsKey("interval") && BTdata["interval"] != 0) { + BTConfig.adaptiveScan = false; + Config_update(BTdata, "interval", BTConfig.BLEinterval); + if (BTConfig.intervalActiveScan < BTConfig.BLEinterval) { + Config_update(BTdata, "interval", BTConfig.intervalActiveScan); + } + } + // Define if the scan is adaptive or not - and avoid intervalacts to be lower than interval + if (BTdata.containsKey("intervalacts") && BTdata["intervalacts"] < BTConfig.BLEinterval) { + BTConfig.adaptiveScan = false; + // Config_update(BTdata, "interval", BTConfig.intervalActiveScan); + BTConfig.intervalActiveScan = BTConfig.BLEinterval; + } else { + Config_update(BTdata, "intervalacts", BTConfig.intervalActiveScan); + } + // Adaptive scan set + Config_update(BTdata, "adaptivescan", BTConfig.adaptiveScan); + // Time before a connect set + Config_update(BTdata, "intervalcnct", BTConfig.intervalConnect); + // publish all BLE devices discovered or only the identified sensors (like temperature sensors) + Config_update(BTdata, "scanduration", BTConfig.scanDuration); + // define the duration for a scan; in milliseconds + Config_update(BTdata, "onlysensors", BTConfig.pubOnlySensors); + // publish devices which randomly change their MAC addresses + Config_update(BTdata, "randommacs", BTConfig.pubRandomMACs); + // Home Assistant presence message topic + Config_update(BTdata, "prestopic", BTConfig.presenceTopic); + // Home Assistant presence message use iBeacon UUID + Config_update(BTdata, "presuseuuid", BTConfig.presenceUseBeaconUuid); + // Timer to trigger a device state as offline if not seen + Config_update(BTdata, "presenceawaytimer", BTConfig.presenceAwayTimer); + // Timer to trigger a device state as offline if not seen + Config_update(BTdata, "movingtimer", BTConfig.movingTimer); + // Force passive scan + Config_update(BTdata, "forcepscn", BTConfig.forcePassiveScan); + // MinRSSI set + Config_update(BTdata, "minrssi", BTConfig.minRssi); + // Send undecoded device data + Config_update(BTdata, "extDecoderEnable", BTConfig.extDecoderEnable); + // Topic to send undecoded device data + Config_update(BTdata, "extDecoderTopic", BTConfig.extDecoderTopic); + // Sets whether to filter publishing + Config_update(BTdata, "filterConnectable", BTConfig.filterConnectable); + // Publish advertisement data + Config_update(BTdata, "pubadvdata", BTConfig.pubAdvData); + // Use iBeacon UUID as topic, instead of sender (random) MAC address + Config_update(BTdata, "pubuuid4topic", BTConfig.pubBeaconUuidForTopic); + // Disable Whitelist & Blacklist + Config_update(BTdata, "ignoreWBlist", (BTConfig.ignoreWBlist)); + // Enable or disable the BT gateway + Config_update(BTdata, "enabled", BTConfig.enabled); + + stateBTMeasures(startup); + + if (BTdata.containsKey("erase") && BTdata["erase"].as()) { + // Erase config from NVS (non-volatile storage) + preferences.begin(Gateway_Short_Name, false); + if (preferences.isKey("BTConfig")) { + int result = preferences.remove("BTConfig"); + Log.notice(F("BT config erase result: %d" CR), result); + preferences.end(); + return; // Erase prevails on save, so skipping save + } else { + preferences.end(); + Log.notice(F("BT config not found" CR)); + } + } + + if (BTdata.containsKey("save") && BTdata["save"].as()) { + StaticJsonDocument jsonBuffer; + JsonObject jo = jsonBuffer.to(); + jo["bleconnect"] = BTConfig.bleConnect; + jo["interval"] = BTConfig.BLEinterval; + jo["adaptivescan"] = BTConfig.adaptiveScan; + jo["intervalacts"] = BTConfig.intervalActiveScan; + jo["intervalcnct"] = BTConfig.intervalConnect; + jo["scanduration"] = BTConfig.scanDuration; + jo["onlysensors"] = BTConfig.pubOnlySensors; + jo["randommacs"] = BTConfig.pubRandomMACs; + jo["hasspresence"] = BTConfig.presenceEnable; + jo["prestopic"] = BTConfig.presenceTopic; + jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; + jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value + jo["extDecoderEnable"] = BTConfig.extDecoderEnable; + jo["extDecoderTopic"] = BTConfig.extDecoderTopic; + jo["filterConnectable"] = BTConfig.filterConnectable; + jo["pubadvdata"] = BTConfig.pubAdvData; + jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; + jo["ignoreWBlist"] = BTConfig.ignoreWBlist; + jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; + jo["movingtimer"] = BTConfig.movingTimer; + jo["forcepscn"] = BTConfig.forcePassiveScan; + jo["enabled"] = BTConfig.enabled; + // Save config into NVS (non-volatile storage) + String conf = ""; + serializeJson(jsonBuffer, conf); + preferences.begin(Gateway_Short_Name, false); + int result = preferences.putString("BTConfig", conf); + preferences.end(); + Log.notice(F("BT config save: %s, result: %d" CR), conf.c_str(), result); + } +} + +void BTConfig_load() { + StaticJsonDocument jsonBuffer; + preferences.begin(Gateway_Short_Name, true); + if (preferences.isKey("BTConfig")) { + auto error = deserializeJson(jsonBuffer, preferences.getString("BTConfig", "{}")); + preferences.end(); + Log.notice(F("BT config loaded" CR)); + if (error) { + Log.error(F("BT config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity()); + return; + } + if (jsonBuffer.isNull()) { + Log.warning(F("BT config is null" CR)); + return; + } + JsonObject jo = jsonBuffer.as(); + BTConfig_fromJson(jo, true); // Never send MQTT message with config + Log.notice(F("BT config loaded" CR)); + } else { + preferences.end(); + Log.notice(F("BT config not found" CR)); + } +} + +void PublishDeviceData(JsonObject& BLEdata); + +atomic_int forceBTScan; + +void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type = 0, const char* name = ""); + +BLEdevice* getDeviceByMac(const char* mac); // Declared here to avoid pre-compilation issue (misplaced auto declaration by pio) +BLEdevice* getDeviceByMac(const char* mac) { + Log.trace(F("getDeviceByMac %s" CR), mac); + + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + if ((strcmp((*it)->macAdr, mac) == 0)) { + return *it; + } + } + return &NO_BT_DEVICE_FOUND; +} + +bool updateWorB(JsonObject& BTdata, bool isWhite) { + Log.trace(F("update WorB" CR)); + const char* jsonKey = isWhite ? "white-list" : "black-list"; + + int size = BTdata[jsonKey].size(); + if (size == 0) + return false; + + for (int i = 0; i < size; i++) { + const char* mac = BTdata[jsonKey][i]; + createOrUpdateDevice(mac, (isWhite ? device_flags_isWhiteL : device_flags_isBlackL), + UNKWNON_MODEL); + } + + return true; +} + +void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type, const char* name) { + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(30000)) == pdFALSE) { + Log.error(F("Semaphore NOT taken" CR)); + return; + } + BLEdevice* device = getDeviceByMac(mac); + if (device == &NO_BT_DEVICE_FOUND) { + Log.trace(F("add %s" CR), mac); + //new device + device = new BLEdevice(); + strcpy(device->macAdr, mac); + device->isDisc = flags & device_flags_isDisc; + device->isWhtL = flags & device_flags_isWhiteL; + device->isBlkL = flags & device_flags_isBlackL; + device->connect = flags & device_flags_connect; + device->macType = mac_type; + // Check name length + if (strlen(name) > 20) { + Log.warning(F("Name too long, truncating" CR)); + strncpy(device->name, name, 20); + device->name[19] = '\0'; + } else { + strcpy(device->name, name); + } + device->sensorModel_id = model; + device->lastUpdate = millis(); + devices.push_back(device); + newDevices++; + } else { + Log.trace(F("update %s" CR), mac); + device->lastUpdate = millis(); + device->macType = mac_type; + + if (flags & device_flags_isDisc) { + device->isDisc = true; + } + + if (flags & device_flags_connect) { + device->connect = true; + } + + if (model != UNKWNON_MODEL && device->sensorModel_id == UNKWNON_MODEL) { + newDevices++; + device->isDisc = false; + device->sensorModel_id = model; + } + + // If a device has been added to the white-list, flag it so it can be auto-detected + if (!device->isWhtL && flags & device_flags_isWhiteL) { + newDevices++; + } + if (flags & device_flags_isWhiteL || flags & device_flags_isBlackL) { + device->isWhtL = flags & device_flags_isWhiteL; + device->isBlkL = flags & device_flags_isBlackL; + } + } + + // update oneWhite flag + oneWhite = oneWhite || device->isWhtL; + + xSemaphoreGive(semaphoreCreateOrUpdateDevice); +} + +void updateDevicesStatus() { + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + BLEdevice* p = *it; + unsigned long now = millis(); + // Check for tracker status + bool isTracker = false; +# if BLEDecoder + std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); + if (tag.length() >= 4) { + isTracker = checkIfIsTracker(tag[3]); + } + // Device tracker devices + if (isTracker) { // We apply the offline status only for tracking device, can be extended further to all the devices + if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.presenceAwayTimer) && (now > BTConfig.presenceAwayTimer)) && + (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = p->macAdr; + BLEdata["state"] = "offline"; + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + // We set the lastUpdate to 0 to avoid replublishing the offline state + p->lastUpdate = 0; + } + } + // Moving detection devices (devices with an accelerometer) + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BC08) { + if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.movingTimer) && (now > BTConfig.movingTimer)) && + (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = p->macAdr; + BLEdata["state"] = "offline"; + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + // We set the lastUpdate to 0 to avoid replublishing the offline state + p->lastUpdate = 0; + } + } +# endif + } +} + +void dumpDevices() { +# if LOG_LEVEL > LOG_LEVEL_NOTICE + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + BLEdevice* p = *it; + Log.trace(F("macAdr %s" CR), p->macAdr); + Log.trace(F("macType %d" CR), p->macType); + Log.trace(F("isDisc %d" CR), p->isDisc); + Log.trace(F("isWhtL %d" CR), p->isWhtL); + Log.trace(F("isBlkL %d" CR), p->isBlkL); + Log.trace(F("connect %d" CR), p->connect); + Log.trace(F("sensorModel_id %d" CR), p->sensorModel_id); + Log.trace(F("LastUpdate %u" CR), p->lastUpdate); + } +# endif +} + +void strupp(char* beg) { + while ((*beg = toupper(*beg))) + ++beg; +} + +# ifdef ZmqttDiscovery +void DT24Discovery(const char* mac, const char* sensorModel_id) { +# define DT24parametersCount 7 + Log.trace(F("DT24Discovery" CR)); + const char* DT24sensor[DT24parametersCount][9] = { + {"sensor", "volt", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "amp", mac, "current", jsonCurrent, "", "", "A", stateClassMeasurement}, + {"sensor", "watt", mac, "power", jsonPower, "", "", "W", stateClassMeasurement}, + {"sensor", "watt-hour", mac, "power", jsonEnergy, "", "", "kWh", stateClassMeasurement}, + {"sensor", "price", mac, "", jsonMsg, "", "", "", stateClassNone}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"binary_sensor", "inUse", mac, "power", jsonInuse, "", "", "", stateClassNone} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, DT24sensor, DT24parametersCount, "DT24", "ATorch", sensorModel_id); +} + +void BM2Discovery(const char* mac, const char* sensorModel_id) { +# define BM2parametersCount 2 + Log.trace(F("BM2Discovery" CR)); + const char* BM2sensor[BM2parametersCount][9] = { + {"sensor", "volt", mac, "voltage", jsonVoltBM2, "", "", "V", stateClassMeasurement}, // We use a json definition that retrieve only data from the BM2 decoder, as this sensor also advertize volt as an iBeacon + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, BM2sensor, BM2parametersCount, "BM2", "Generic", sensorModel_id); +} + +void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) { +# define LYWSD03MMCparametersCount 4 + Log.trace(F("LYWSD03MMCDiscovery" CR)); + const char* LYWSD03MMCsensor[LYWSD03MMCparametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, LYWSD03MMCsensor, LYWSD03MMCparametersCount, "LYWSD03MMC", "Xiaomi", sensorModel); +} + +void MHO_C401Discovery(const char* mac, const char* sensorModel) { +# define MHO_C401parametersCount 4 + Log.trace(F("MHO_C401Discovery" CR)); + const char* MHO_C401sensor[MHO_C401parametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, MHO_C401sensor, MHO_C401parametersCount, "MHO_C401", "Xiaomi", sensorModel); +} + +void HHCCJCY01HHCCDiscovery(const char* mac, const char* sensorModel) { +# define HHCCJCY01HHCCparametersCount 5 + Log.trace(F("HHCCJCY01HHCCDiscovery" CR)); + const char* HHCCJCY01HHCCsensor[HHCCJCY01HHCCparametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "lux", mac, "illuminance", jsonLux, "", "", "lx", stateClassMeasurement}, + {"sensor", "fer", mac, "", jsonFer, "", "", "µS/cm", stateClassMeasurement}, + {"sensor", "moi", mac, "", jsonMoi, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, HHCCJCY01HHCCsensor, HHCCJCY01HHCCparametersCount, "HHCCJCY01HHCC", "Xiaomi", sensorModel); +} + +void XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel) { +# define XMWSDJ04MMCparametersCount 4 + Log.trace(F("XMWSDJ04MMCDiscovery" CR)); + const char* XMWSDJ04MMCsensor[XMWSDJ04MMCparametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, XMWSDJ04MMCsensor, XMWSDJ04MMCparametersCount, "XMWSDJ04MMC", "Xiaomi", sensorModel); +} + +# else +void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) {} +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 XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel_id) {} +# endif + +/* + Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleScan.cpp + Ported to Arduino ESP32 by Evandro Copercini + */ +// core task implementation thanks to https://techtutorialsx.com/2017/05/09/esp32-running-code-on-a-specific-core/ + +//core on which the BLE detection task will run +static int taskCore = 0; + +class ScanCallbacks : public NimBLEScanCallbacks { + void onResult(const NimBLEAdvertisedDevice* advertisedDevice) { + NimBLEAdvertisedDevice* ad = new NimBLEAdvertisedDevice(*advertisedDevice); + if (xQueueSend(BLEQueue, &ad, 0) != pdTRUE) { + Log.error(F("BLEQueue full" CR)); + delete (ad); + } + } +} scanCallbacks; + +std::string convertServiceData(std::string deviceServiceData) { + int serviceDataLength = (int)deviceServiceData.length(); + char spr[2 * serviceDataLength + 1]; + for (int i = 0; i < serviceDataLength; i++) sprintf(spr + 2 * i, "%.2x", (unsigned char)deviceServiceData[i]); + spr[2 * serviceDataLength] = 0; + Log.trace(F("Converted service data (%d) to %s" CR), serviceDataLength, spr); + return spr; +} + +bool checkIfIsTracker(char ch) { + uint8_t data = 0; + if (ch >= '0' && ch <= '9') + data = ch - '0'; + else if (ch >= 'a' && ch <= 'f') + data = 10 + (ch - 'a'); + + if (((data >> 3) & 0x01) == 1) { + Log.trace(F("Is Device Tracker" CR)); + return true; + } else { + return false; + } +} + +void procBLETask(void* pvParameters) { + BLEAdvertisedDevice* advertisedDevice = nullptr; + + for (;;) { + xQueueReceive(BLEQueue, &advertisedDevice, portMAX_DELAY); + // Feed the watchdog + //esp_task_wdt_reset(); + if (!BTProcessLock) { + Log.trace(F("Creating BLE buffer" CR)); + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = advertisedDevice->getAddress().toString(); + BLEdata["mac_type"] = advertisedDevice->getAddress().getType(); + BLEdata["adv_type"] = advertisedDevice->getAdvType(); + Log.notice(F("BT Device detected: %s" CR), BLEdata["id"].as()); + BLEdevice* device = getDeviceByMac(BLEdata["id"].as()); + + if (BTConfig.filterConnectable && device->connect) { + Log.notice(F("Filtered connectable device" CR)); + delete (advertisedDevice); + continue; + } + + if (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(device)) && !isBlack(device))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC) + if (advertisedDevice->haveName()) + BLEdata["name"] = (char*)advertisedDevice->getName().c_str(); + if (advertisedDevice->haveManufacturerData()) { + BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString((uint8_t*)advertisedDevice->getManufacturerData().data(), + advertisedDevice->getManufacturerData().length()); + } + BLEdata["rssi"] = (int)advertisedDevice->getRSSI(); + if (advertisedDevice->haveTXPower()) + BLEdata["txpower"] = (int8_t)advertisedDevice->getTXPower(); + if (BTConfig.presenceEnable) { + hass_presence(BLEdata); // with either only sensors or not we can use it for home assistant room presence component + } + if (advertisedDevice->haveServiceData()) { + int serviceDataCount = advertisedDevice->getServiceDataCount(); + Log.trace(F("Get services data number: %d" CR), serviceDataCount); + for (int j = 0; j < serviceDataCount; j++) { + StaticJsonDocument BLEdataBufferTemp; + JsonObject BLEdataTemp = BLEdataBufferTemp.to(); + BLEdataBufferTemp = BLEdataBuffer; + std::string service_data = convertServiceData(advertisedDevice->getServiceData(j)); + Log.trace(F("Service data: %s" CR), service_data.c_str()); + std::string serviceDatauuid = advertisedDevice->getServiceDataUUID(j).toString(); + Log.trace(F("Service data UUID: %s" CR), (char*)serviceDatauuid.c_str()); + BLEdataTemp["servicedata"] = (char*)service_data.c_str(); + BLEdataTemp["servicedatauuid"] = (char*)serviceDatauuid.c_str(); + PublishDeviceData(BLEdataTemp); + } + } else { + PublishDeviceData(BLEdata); + } + } else { + Log.trace(F("Filtered MAC device" CR)); + } + updateDevicesStatus(); + } + delete (advertisedDevice); + vTaskDelay(10); + } +} + +/** + * BLEscan used to retrieve BLE advertized data from devices without connection + */ +void BLEscan() { + // Don't start the next scan until processing of previous results is complete. + while (uxQueueMessagesWaiting(BLEQueue) || queueLength != 0) { // the criteria on queueLength could be adjusted to parallelize the scan and the queue processing + delay(1); // Wait for queue to empty, a yield here instead of the delay cause the WDT to trigger + } + Log.notice(F("Scan begin" CR)); + BLEScan* pBLEScan = BLEDevice::getScan(); + pBLEScan->setScanCallbacks(&scanCallbacks); + if ((millis() > (timeBetweenActive + BTConfig.intervalActiveScan) || BTConfig.intervalActiveScan == BTConfig.BLEinterval) && !BTConfig.forcePassiveScan) { + pBLEScan->setActiveScan(true); + timeBetweenActive = millis(); + } else { + pBLEScan->setActiveScan(false); + } + pBLEScan->setInterval(BLEScanInterval); + pBLEScan->setWindow(BLEScanWindow); + NimBLEScanResults foundDevices = pBLEScan->getResults(BTConfig.scanDuration, false); + if (foundDevices.getCount()) + scanCount++; + Log.notice(F("Found %d devices, scan number %d end" CR), foundDevices.getCount(), scanCount); + Log.trace(F("Process BLE stack free: %u" CR), uxTaskGetStackHighWaterMark(xProcBLETaskHandle)); +} + +/** + * Connect to BLE devices and initiate the callbacks with a service/characteristic request + */ +# if BLEDecoder +void BLEconnect() { + if (!BTProcessLock) { + Log.notice(F("BLE Connect begin" CR)); + do { + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + BLEdevice* p = *it; + if (p->connect) { + Log.trace(F("Model to connect found: %s" CR), p->macAdr); + NimBLEAddress addr((const char*)p->macAdr, p->macType); + if (p->sensorModel_id == BLEconectable::id::LYWSD03MMC || + p->sensorModel_id == BLEconectable::id::MHO_C401) { + LYWSD03MMC_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + BLEclient.publishData(); + } else if (p->sensorModel_id == BLEconectable::id::DT24_BLE) { + DT24_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + BLEclient.publishData(); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { + BM2_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); + BLEclient.publishData(); + } else if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { + XMWSDJ04MMC_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + BLEclient.publishData(); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1) { + SBS1_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT) { + SBBT_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU) { + SBCU_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + } else { + GENERIC_connect BLEclient(addr); + if (BLEclient.processActions(BLEactions)) { + // If we don't regularly connect to this, disable connections so advertisements + // won't be filtered if BLE_FILTER_CONNECTABLE is set. + p->connect = false; + } + } + if (BLEactions.size() > 0) { + std::vector swap; + for (auto& it : BLEactions) { + if (!it.complete && --it.ttl) { + swap.push_back(it); + } else if (it.addr == NimBLEAddress(p->macAdr, p->macType)) { + if (p->sensorModel_id != BLEconectable::id::DT24_BLE && + 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 != BLEconectable::id::MHO_C401 && + p->sensorModel_id != BLEconectable::id::XMWSDJ04MMC) { + // if irregulary connected to and connection failed clear the connect flag. + p->connect = false; + } + } + } + std::swap(BLEactions, swap); + } + } + } + } while (BLEactions.size() > 0); + Log.notice(F("BLE Connect end" CR)); + } +} +# else +void BLEconnect() {} +# endif + +void stopProcessing(bool deinit) { + if (BTConfig.enabled) { + BTProcessLock = true; + // We stop the scan + Log.notice(F("Stopping BLE scan" CR)); + BLEScan* pBLEScan = BLEDevice::getScan(); + if (pBLEScan->isScanning()) { + pBLEScan->stop(); + } + + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { + Log.notice(F("Stopping BLE tasks" CR)); + //Suspending, deleting tasks and stopping BT to free memory + vTaskSuspend(xCoreTaskHandle); + vTaskDelete(xCoreTaskHandle); + vTaskSuspend(xProcBLETaskHandle); + vTaskDelete(xProcBLETaskHandle); + xSemaphoreGive(semaphoreBLEOperation); + } + // Using deinit to free memory, should only be used if we are going to restart the gateway + if (deinit) + BLEDevice::deinit(true); + } + Log.notice(F("BLE gateway stopped, free heap: %d" CR), ESP.getFreeHeap()); +} + +void coreTask(void* pvParameters) { + while (true) { + if (!BTProcessLock) { + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(30000)) == pdTRUE) { + BLEscan(); + // Launching a connect every TimeBtwConnect + if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { + timeBetweenConnect = millis(); + BLEconnect(); + } + //dumpDevices(); + Log.trace(F("CoreTask stack free: %u" CR), uxTaskGetStackHighWaterMark(xCoreTaskHandle)); + xSemaphoreGive(semaphoreBLEOperation); + } else { + Log.error(F("Failed to start scan - BLE busy" CR)); + } + if (SYSConfig.powerMode > 0) { + int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); // is this enough, it will wait the full deepsleep... + if (scan == 1) BTforceScan(); + ready_to_sleep = true; + } else { + for (int interval = BTConfig.BLEinterval, waitms; interval > 0; interval -= waitms) { + int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); + if (scan == 1) BTforceScan(); // should we break after this? + delay(waitms = interval > 100 ? 100 : interval); // 100ms + } + } + } + delay(1); + } +} + +void setupBTTasksAndBLE() { +# ifdef CONFIG_BTDM_BLE_SCAN_DUPL + BLEDevice::setScanDuplicateCacheSize(BLEScanDuplicateCacheSize); +# endif + BLEDevice::init(""); + xTaskCreateUniversal( + procBLETask, /* Function to implement the task */ + "procBLETask", /* Name of the task */ +# if defined(USE_ESP_IDF) || defined(USE_BLUFI) + 14500, +# else + 9500, /* Stack size in bytes */ +# endif + NULL, /* Task input parameter */ + 2, /* Priority of the task (set higher than core task) */ + &xProcBLETaskHandle, /* Task handle. */ + 1); /* Core where the task should run */ + + // we setup a task with priority one to avoid conflict with other gateways + xTaskCreateUniversal( + coreTask, /* Function to implement the task */ + "coreTask", /* Name of the task */ + 5120, /* Stack size in bytes */ + NULL, /* Task input parameter */ + 1, /* Priority of the task */ + &xCoreTaskHandle, /* Task handle. */ + taskCore); /* Core where the task should run */ +} + +void setupBT() { + BTConfig_init(); + BTConfig_load(); + Log.notice(F("BLE scans interval: %d" CR), BTConfig.BLEinterval); + Log.notice(F("BLE connects interval: %d" CR), BTConfig.intervalConnect); + Log.notice(F("BLE scan duration: %d" CR), BTConfig.scanDuration); + Log.notice(F("Publishing only BLE sensors: %T" CR), BTConfig.pubOnlySensors); + Log.notice(F("Publishing random MAC devices: %T" CR), BTConfig.pubRandomMACs); + Log.notice(F("Adaptive BLE scan: %T" CR), BTConfig.adaptiveScan); + Log.notice(F("Active BLE scan interval: %d" CR), BTConfig.intervalActiveScan); + Log.notice(F("minrssi: %d" CR), -abs(BTConfig.minRssi)); + Log.notice(F("Presence Away Timer: %d" CR), BTConfig.presenceAwayTimer); + Log.notice(F("Moving Timer: %d" CR), BTConfig.movingTimer); + Log.notice(F("Force passive scan: %T" CR), BTConfig.forcePassiveScan); + Log.notice(F("Enabled BLE: %T" CR), BTConfig.enabled); + + atomic_init(&forceBTScan, 0); // in theory, we don't need this + + semaphoreCreateOrUpdateDevice = xSemaphoreCreateBinary(); + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + + semaphoreBLEOperation = xSemaphoreCreateBinary(); + xSemaphoreGive(semaphoreBLEOperation); + + BLEQueue = xQueueCreate(QueueSize, sizeof(NimBLEAdvertisedDevice*)); + if (BTConfig.enabled) { + setupBTTasksAndBLE(); + Log.notice(F("gatewayBT multicore ESP32 setup done" CR)); + } else { + Log.notice(F("gatewayBT multicore ESP32 setup disabled" CR)); + } +} + +boolean valid_service_data(const char* data, int size) { + for (int i = 0; i < size; ++i) { + if (data[i] != 48) // 48 correspond to 0 in ASCII table + return true; + } + return false; +} + +# if defined(ZmqttDiscovery) && BLEDecoder == true +// This function always should be called from the main core as it generates direct mqtt messages +// When overrideDiscovery=true, we publish discovery messages of known devices (even if no new) +void launchBTDiscovery(bool overrideDiscovery) { + if (!overrideDiscovery && newDevices == 0) + return; + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdFALSE) { + Log.error(F("Semaphore NOT taken" CR)); + return; + } + newDevices = 0; + vector localDevices = devices; + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + for (vector::iterator it = localDevices.begin(); it != localDevices.end(); ++it) { + BLEdevice* p = *it; + Log.trace(F("Device mac %s" CR), p->macAdr); + // Do not launch discovery for the devices already discovered (unless we have overrideDiscovery) or that are not unique by their MAC Address (iBeacon, GAEN and Microsoft CDP) + if (overrideDiscovery || !isDiscovered(p)) { + String macWOdots = String(p->macAdr); + macWOdots.replace(":", ""); + if (p->sensorModel_id >= 0) { + Log.trace(F("Looking for Model_id: %d" CR), p->sensorModel_id); + std::string properties = decoder.getTheengProperties(p->sensorModel_id); + Log.trace(F("properties: %s" CR), properties.c_str()); + std::string brand = decoder.getTheengAttribute(p->sensorModel_id, "brand"); + std::string model = decoder.getTheengAttribute(p->sensorModel_id, "model"); + if (displayDeviceName || ForceDeviceName) { + if (p->name[0] != '\0') { + model = p->name; + } + } + std::string model_id = decoder.getTheengAttribute(p->sensorModel_id, "model_id"); + + // Check for tracker status + bool isTracker = false; + std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); + if (tag.length() >= 4) { + isTracker = checkIfIsTracker(tag[3]); + } + + String discovery_topic = String(subjectBTtoMQTT) + "/" + macWOdots; + 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 + if (isTracker) { + String tracker_name = String(model_id.c_str()) + "-tracker"; + String tracker_id = macWOdots + "-tracker"; + createDiscovery("device_tracker", + discovery_topic.c_str(), tracker_name.c_str(), 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 == TheengsDecoder::BLE_ID_NUM::BC08) { + String sensor_name = String(model_id.c_str()) + "-moving"; + String sensor_id = macWOdots + "-moving"; + createDiscovery("binary_sensor", + discovery_topic.c_str(), sensor_name.c_str(), sensor_id.c_str(), + will_Topic, "moving", "{% if value_json.get('accx') -%}on{%- else -%}off{%- endif %}", + "on", "off", "", + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } + if (!properties.empty()) { + StaticJsonDocument jsonBuffer; + auto error = deserializeJson(jsonBuffer, properties); + if (error) { + if (jsonBuffer.overflowed()) { + // This should not happen if JSON_MSG_BUFFER is large enough for + // the Theengs json properties + Log.error(F("JSON deserialization of Theengs properties overflowed (error %s), buffer capacity: %u. Program might crash. Properties json: %s" CR), + error.c_str(), jsonBuffer.capacity(), properties.c_str()); + } else { + Log.error(F("JSON deserialization of Theengs properties errored: %" CR), + error.c_str()); + } + } + for (JsonPair prop : jsonBuffer["properties"].as()) { + Log.trace(F("Key: %s"), prop.key().c_str()); + Log.trace(F("Unit: %s"), prop.value()["unit"].as()); + Log.trace(F("Name: %s"), prop.value()["name"].as()); + String entity_name = ""; + if (displayDeviceName || ForceDeviceName) { + entity_name = String(model.c_str()) + "-" + String(prop.key().c_str()); + } else { + entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); + } + String unique_id = macWOdots + "-" + String(prop.key().c_str()); + String value_template = "{{ value_json." + String(prop.key().c_str()) + " | is_defined }}"; + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1 && strcmp(prop.key().c_str(), "state") == 0) { + String payload_on = "{\"model_id\":\"X1\",\"cmd\":\"on\",\"id\":\"" + String(p->macAdr) + "\"}"; + String payload_off = "{\"model_id\":\"X1\",\"cmd\":\"off\",\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("switch", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "switch", value_template.c_str(), + payload_on.c_str(), payload_off.c_str(), "", 0, + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone, "off", "on"); + unique_id = macWOdots + "-press"; + entity_name = String(model_id.c_str()) + "-press"; + String payload_press = "{\"model_id\":\"X1\",\"cmd\":\"press\",\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("button", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "button", "", + payload_press.c_str(), "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT && strcmp(prop.key().c_str(), "open") == 0) { + value_template = "{% if value_json.direction == \"up\" -%} {{ 100 - value_json.open/2 }}{% elif value_json.direction == \"down\" %}{{ value_json.open/2 }}{% else %} {{ value_json.open/2 }}{%- endif %}"; + String command_template = "{\"model_id\":\"W270160X\",\"tilt\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("cover", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "cover", value_template.c_str(), + "50", "", "", 0, + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + "blind", nullptr, nullptr, nullptr, command_template.c_str()); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU && strcmp(prop.key().c_str(), "position") == 0) { + String command_template = "{\"model_id\":\"W070160X\",\"position\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("cover", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "cover", "{{ value_json.position }}", + "0", "100", "", 0, + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + "curtain", nullptr, nullptr, nullptr, command_template.c_str()); + } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && + strcmp(prop.key().c_str(), "weighing_mode") == 0) { + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "enum", value_template.c_str(), + "", "", prop.value()["unit"], + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassMeasurement, nullptr, nullptr, "[\"person\",\"object\"]"); + } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && + strcmp(prop.key().c_str(), "unit") == 0) { + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "enum", value_template.c_str(), + "", "", prop.value()["unit"], + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassMeasurement, nullptr, nullptr, "[\"lb\",\"kg\",\"jin\"]"); + } else if (strcmp(prop.value()["unit"], "string") == 0 && strcmp(prop.key().c_str(), "mac") != 0) { + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "", "", "", + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (p->sensorModel_id == TheengsDecoder::MUE4094RT && strcmp(prop.value()["unit"], "status") == 0) { // This device does not a broadcast when there is nothing detected so adding a timeout + createDiscovery("binary_sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "True", "False", "", + BTConfig.presenceAwayTimer / 1000, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (strcmp(prop.value()["unit"], "status") == 0) { + createDiscovery("binary_sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "True", "False", "", + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (strcmp(prop.key().c_str(), "device") != 0 && strcmp(prop.key().c_str(), "mac") != 0) { // Exception on device and mac as these ones are not sensors + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "", "", prop.value()["unit"], + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassMeasurement); + } + } + } + } 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) { + // 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"); + } + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { + // Sensor discovery + BM2Discovery(macWOdots.c_str(), "BM2"); + // Device tracker discovery + String tracker_id = macWOdots + "-tracker"; + createDiscovery("device_tracker", + discovery_topic.c_str(), "BM2-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"); + } + if (p->sensorModel_id == BLEconectable::id::MHO_C401) { + MHO_C401Discovery(macWOdots.c_str(), "MHO-C401"); + } + if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { + XMWSDJ04MMCDiscovery(macWOdots.c_str(), "XMWSDJ04MMC"); + } + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC) { + HHCCJCY01HHCCDiscovery(macWOdots.c_str(), "HHCCJCY01HHCC"); + } + } else { + Log.trace(F("Device UNKNOWN_MODEL %s" CR), p->macAdr); + } + } + } + p->isDisc = true; // we don't need the semaphore and all the search magic via createOrUpdateDevice + } else { + Log.trace(F("Device already discovered or that doesn't require discovery %s" CR), p->macAdr); + } + } +} +# else +void launchBTDiscovery(bool overrideDiscovery) {} +# endif + +# if BLEDecryptor +// ** TODO - Hex string to bytes, there is probably a function for this already just need to find it +int hexToBytes(String hex, uint8_t *out, size_t maxLen) { + int len = hex.length(); + if (len % 2 || len / 2 > maxLen) return -1; + for (int i = 0, j = 0; i < len; i += 2, j++) { + out[j] = (uint8_t) strtol(hex.substring(i, i + 2).c_str(), nullptr, 16); + } + return len / 2; +} +// Reverse bytes +void reverseBytes(uint8_t *data, size_t length) { + size_t i; + for (i = 0; i < length / 2; i++) { + uint8_t temp = data[i]; + data[i] = data[length - 1 - i]; + data[length - 1 - i] = temp; + } +} +# endif + +# if BLEDecoder +void process_bledata(JsonObject& BLEdata) { + yield(); // Necessary to let the loop run in case of connectivity issues + if (!BLEdata.containsKey("id")) { + Log.error(F("No mac address in the payload" CR)); + return; + } + const char* mac = BLEdata["id"].as(); + Log.trace(F("Processing BLE data %s" CR), BLEdata["id"].as()); + int model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); + int mac_type = BLEdata["mac_type"].as(); + +# if BLEDecryptor + if (BLEdata["encr"] && (BLEdata["encr"].as() >0 && BLEdata["encr"].as() <=2)) { + // Decrypting Encrypted BLE Data PVVX, BTHome or Victron + Log.trace(F("[BLEDecryptor] Decrypt ENCR:%d ModelID:%s Payload:%s" CR), BLEdata["encr"].as(), BLEdata["model_id"].as(), BLEdata["cipher"].as()); + + // MAC address + String macWOdots = BLEdata["id"].as(); // Mac Address without dots + macWOdots.replace(":", ""); + unsigned char macAddress[6]; + int maclen = hexToBytes(macWOdots, macAddress, 6); + if (maclen != 6) { + Log.error(F("[BLEDecryptor] Invalid MAC Address length %d" CR), maclen); + return; + } + + // AES decryption key + unsigned char bleaeskey[16]; + int bleaeskeylength = 0; + if (ble_aes_keys.containsKey(macWOdots)){ + Log.trace(F("[BLEDecryptor] Custom AES key %s" CR), ble_aes_keys[macWOdots].as()); + bleaeskeylength = hexToBytes(ble_aes_keys[macWOdots], bleaeskey, 16); + } else { + Log.trace(F("[BLEDecryptor] Default AES key" CR)); + bleaeskeylength = hexToBytes(ble_aes, bleaeskey, 16); + } + // Check AES Key + if (bleaeskeylength != 16) { + Log.error(F("[BLEDecryptor] Invalid key length %d" CR), bleaeskeylength); + return; + } + + // Build nonce and aad + uint8_t nonce[16]; + int noncelength = 0; + unsigned char aad[1]; + int aadLength; + + if (BLEdata["encr"].as() == 1){ // PVVX Encrypted + noncelength = 11; // 11 bytes + reverseBytes(macAddress, 6); // 6 bytes: device address in reverse + memcpy(nonce, macAddress, 6); + int maclen = hexToBytes(macWOdots, macAddress, 6); + + unsigned char servicedata[16]; + int servicedatalen = hexToBytes(BLEdata["servicedata"].as(), servicedata, 16); + nonce[6] = servicedatalen + 3; // 1 byte : length of (service data + type and UUID) + nonce[7] = 0x16; // 1 byte : "16" -> AD type for "Service Data - 16-bit UUID" + nonce[8] = 0x1A; // 2 bytes: "1a18" -> UUID 181a in little-endian + nonce[9] = 0x18; // + unsigned char ctr[1]; // 1 byte : counter + int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 1); + if (ctrlen != 1) { + Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); + return; + } + nonce[10] = ctr[0]; + aad[0] = 0x11; + aadLength = 1; + Log.trace(F("[BLEDecryptor] PVVX nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); + + } else if (BLEdata["encr"].as() == 2){ // BTHome V2 Encrypted + noncelength = 13; // 13 bytes + memcpy(nonce, macAddress, 6); + nonce[6] = 0xD2; // UUID + nonce[7] = 0xFC; + nonce[8] = 0x41; // BTHome Device Data encrypted payload byte + unsigned char ctr[4]; // Counter + int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 4); + if (ctrlen != 4) { + Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); + return; + } + memcpy(&nonce[9], ctr, 4); + aad[0] = 0x00; + aadLength = 0; + Log.trace(F("[BLEDecryptor] BTHomeV2 nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); + + } else if (BLEdata["encr"].as() == 3){ + nonce[16] = {0}; // Victron has a 16 byte zero padded nonce with IV bytes 6,7 + unsigned char iv[2]; + int ivlen = hexToBytes(BLEdata["ctr"].as(), iv, 2); + if (ivlen != 2) { + Log.error(F("[BLEDecryptor] Invalid iv length %d" CR), ivlen); + return; + } + memcpy(nonce, iv, 2); + Log.trace(F("[BLEDecryptor] Victron nonce %s" CR), NimBLEUtils::dataToHexString(nonce, 16).c_str()); + } else { + return; // No match + } + + // Ciphertext to bytes + int cipherlen = sizeof(BLEdata["cipher"].as()); + unsigned char ciphertext[cipherlen]; + int ciphertextlen = hexToBytes(BLEdata["cipher"].as(), ciphertext, cipherlen); + unsigned char decrypted[ciphertextlen]; // Decrypted payload + + // Decrypt ciphertext + if (BLEdata["encr"].as() == 1 || BLEdata["encr"].as() == 2) { + // Decrypt PVVX and BTHome V2 ciphertext using AES CCM + mbedtls_ccm_context ctx; + mbedtls_ccm_init(&ctx); + if (mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, bleaeskey, 128) != 0) { + Log.error(F("[BLEDecryptor] Failed to set AES key to mbedtls" CR)); + return; + } + + // Message Integrity Check (MIC) + unsigned char mic[4]; + int miclen = hexToBytes(BLEdata["mic"].as(), mic, 4); + if (miclen != 4) { + Log.error(F("[BLEDecryptor] Invalid MIC length %d" CR), miclen); + return; + } + + int ret = mbedtls_ccm_auth_decrypt( + &ctx, // AES Key + ciphertextlen, // length of ciphertext + nonce, noncelength, // Nonce + aad, aadLength, // AAD + ciphertext, // input ciphertext + decrypted, // output plaintext + mic, sizeof(mic) // Message Integrity Check + ); + mbedtls_ccm_free(&ctx); + + if (ret == 0) { + Log.notice(F("[BLEDecryptor] Decryption successful" CR)); + } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { + Log.error(F("[BLEDecryptor] Authentication failed." CR)); + return; + } else { + Log.error(F("[BLEDecryptor] Decryption failed with error: %X" CR), ret); + return; + } + + // Build new servicedata + if (BLEdata["encr"].as() == 1){ // PVVX + BLEdata["servicedata"] = NimBLEUtils::dataToHexString(decrypted, ciphertextlen); + } else if (BLEdata["encr"].as() == 2) { // BTHomeV2 + // Build new servicedata + uint8_t newservicedata[3 + ciphertextlen]; + newservicedata[0] = 0x40; // Decrypted BTHomeV2 Packet Type + newservicedata[1] = 0x00; // Packet counter which the PVVX BTHome non-encrypted has but the encrypted does not + newservicedata[2] = 0x00; // **TODO Convert the ctr to the packet counter or just stick with 0? + memcpy(&newservicedata[3], decrypted, ciphertextlen); + BLEdata["servicedata"] = NimBLEUtils::dataToHexString(newservicedata, ciphertextlen + 3); + } else { + return; + } + Log.trace(F("[BLEDecryptor] Decrypted servicedata %s" CR), BLEdata["servicedata"].as()); + + } else if (BLEdata["encr"].as() == 3) { + // Decrypt Victron Energy encrypted advertisements. + size_t nc_off = 0; + uint8_t stream_block[16] = {0}; + + mbedtls_aes_context ctx; + mbedtls_aes_init(&ctx); + mbedtls_aes_setkey_enc(&ctx, bleaeskey, 128); + int ret = mbedtls_aes_crypt_ctr( + &ctx, // AES Key + ciphertextlen, // length of ciphertext + &nc_off, + nonce, // 16 byte nonce with 2 bytes iv + stream_block, + ciphertext, // input ciphertext + decrypted // output plaintext + ); + mbedtls_aes_free(&ctx); + + if (ret == 0) { + Log.notice(F("[BLEDecryptor] Victron Decryption successful" CR)); + } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { + Log.error(F("[BLEDecryptor] Victron Authentication failed." CR)); + return; + } else { + Log.error(F("[BLEDecryptor] Victron decryption failed with error: %X" CR), ret); + return; + } + + // Build new manufacturerdata + unsigned char manufacturerdata[10 + ciphertextlen]; + int manufacturerdatalen = hexToBytes(BLEdata["manufacturerdata"].as(), manufacturerdata, 10); + manufacturerdata[2] = 0x11; // Replace byte 2 with "11" indicate decrypted data + manufacturerdata[7] = 0xff; // Replace byte 7 with "ff" to indicate decrypted data + manufacturerdata[8] = 0xff; // Replace byte 8 with "ff" to indicate decrypted data + memcpy(&manufacturerdata[8], decrypted, ciphertextlen); // Append the decrypted payload to the manufacturer data + BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString(manufacturerdata, 10 + ciphertextlen); // Rebuild manufacturerdata + Log.trace(F("[BLEDecryptor] Victron decrypted manufacturerdata %s" CR), BLEdata["manufacturerdata"].as()); + } + + // Print before and after decoder post decryption + // serializeJsonPretty(BLEdata, Serial); + model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); + // serializeJsonPretty(BLEdata, Serial); + Log.trace(F("[BLEDecryptor] Decrypted model_id %d" CR), model_id); + + // Remove the cipher fields from BLEdata + BLEdata.remove("encr"); + BLEdata.remove("cipher"); + BLEdata.remove("ctr"); + BLEdata.remove("mic"); + + } +# endif + + // Convert prmacs to RMACS until or if OMG gets Identity MAC/IRK decoding + if (BLEdata["prmac"]) { + BLEdata.remove("prmac"); + if (BLEdata["track"]) { + BLEdata.remove("track"); + } + BLEdata["type"] = "RMAC"; + Log.trace(F("Potential RMAC (prmac) converted to RMAC" CR)); + } + const char* deviceName = BLEdata["name"] | ""; + + 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 + 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 + createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); + } else { + createOrUpdateDevice(mac, device_flags_init, model_id, mac_type, deviceName); + if (BTConfig.adaptiveScan == true && (BTConfig.BLEinterval != MinTimeBtwScan || BTConfig.intervalActiveScan != MinTimeBtwScan)) { + if (BLEdata.containsKey("acts") && BLEdata.containsKey("cont")) { + if (BLEdata["acts"] && BLEdata["cont"]) { + BTConfig.BLEinterval = MinTimeBtwScan; + BTConfig.intervalActiveScan = MinTimeBtwScan; + BTConfig.scanDuration = MinScanDuration; + Log.notice(F("Active and continuous scanning required, parameters adapted" CR)); + // stateBTMeasures(false); + } + } else if (BLEdata.containsKey("cont") && BTConfig.BLEinterval != MinTimeBtwScan) { + if (BLEdata["cont"]) { + BTConfig.BLEinterval = MinTimeBtwScan; + if ((BLEdata["type"].as()).compare("CTMO") == 0) { + BTConfig.scanDuration = MinScanDuration; + } + Log.notice(F("Passive continuous scanning required, parameters adapted" CR)); + // stateBTMeasures(false); + } + } + } + } + } else { + if (BLEdata.containsKey("name")) { // Connectable only devices + std::string name = BLEdata["name"]; + if (name.compare("LYWSD03MMC") == 0) + model_id = BLEconectable::id::LYWSD03MMC; + else if (name.compare("DT24-BLE") == 0) + model_id = BLEconectable::id::DT24_BLE; + else if (name.compare("MHO-C401") == 0) + model_id = BLEconectable::id::MHO_C401; + else if (name.compare("XMWSDJ04MMC") == 0) + model_id = BLEconectable::id::XMWSDJ04MMC; + + if (model_id > 0) { + Log.trace(F("Connectable device found: %s" CR), name.c_str()); + createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); + } + } else if (BTConfig.extDecoderEnable && model_id < 0 && BLEdata.containsKey("servicedata")) { + const char* service_data = (const char*)(BLEdata["servicedata"] | ""); + if (strstr(service_data, "209800") != NULL) { + model_id = TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC; + Log.trace(F("Connectable device found: HHCCJCY01HHCC" CR)); + createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); + } + } + } + } else { + Log.trace(F("Random MAC or iBeacon device filtered" CR)); + } + if (!BTConfig.extDecoderEnable && model_id < 0) { + Log.trace(F("No eligible device found " CR)); + } +} +void PublishDeviceData(JsonObject& BLEdata) { + if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough + // Decode the payload + process_bledata(BLEdata); + // If the device is a random MAC and pubRandomMACs is false we don't publish this payload + if (!BTConfig.pubRandomMACs && (BLEdata["type"].as()).compare("RMAC") == 0) { + Log.trace(F("Random MAC, device filtered" CR)); + return; + } + // If pubAdvData is false we don't publish the adv data + if (!BTConfig.pubAdvData) { + BLEdata.remove("servicedatauuid"); + BLEdata.remove("servicedata"); + BLEdata.remove("manufacturerdata"); + BLEdata.remove("mac_type"); + BLEdata.remove("adv_type"); + // tag device properties + // BLEdata.remove("type"); type is used by the WebUI module to determine the template used to display the signal + BLEdata.remove("cidc"); + BLEdata.remove("acts"); + BLEdata.remove("cont"); + BLEdata.remove("track"); + BLEdata.remove("ctrl"); + } + // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid + if (BLEdata.containsKey("distance")) { + if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { + BLEdata["mac"] = BLEdata["id"].as(); + BLEdata["id"] = BLEdata["uuid"].as(); + } + String topic = String(mqtt_topic) + BTConfig.presenceTopic + String(gateway_name); + Log.trace(F("Pub HA Presence %s" CR), topic.c_str()); + BLEdata["topic"] = topic; + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } + + // If the device is not a sensor and pubOnlySensors is true we don't publish this payload + if (!BTConfig.pubOnlySensors || BLEdata.containsKey("model") || !BLEDecoder) { // Identified device + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } else { + Log.notice(F("Not a sensor device filtered" CR)); + return; + } + +# if BLEDecoder + if (enableMultiGTWSync && BLEdata.containsKey("model_id") && BLEdata.containsKey("id")) { + // Publish tracker sync message + bool isTracker = false; + std::string tag = decoder.getTheengAttribute(BLEdata["model_id"].as(), "tag"); + if (tag.length() >= 4) { + isTracker = checkIfIsTracker(tag[3]); + } + + if (isTracker) { + StaticJsonDocument BLEdataBuffer; + JsonObject TrackerSyncdata = BLEdataBuffer.to(); + TrackerSyncdata["gatewayid"] = gateway_name; + TrackerSyncdata["trackerid"] = BLEdata["id"].as(); + String topic = String(mqtt_topic) + String(subjectTrackerSync); + TrackerSyncdata["topic"] = topic.c_str(); + enqueueJsonObject(TrackerSyncdata); + } + } +# endif + } else { + Log.notice(F("Low rssi, device filtered" CR)); + return; + } +} +# else +void process_bledata(JsonObject& BLEdata) {} +void PublishDeviceData(JsonObject& BLEdata) { + if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough + // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid + if (BLEdata.containsKey("distance")) { + if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { + BLEdata["mac"] = BLEdata["id"].as(); + BLEdata["id"] = BLEdata["uuid"].as(); + } + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } else { + Log.notice(F("Low rssi, device filtered" CR)); + return; + } +} +# endif + +void hass_presence(JsonObject& HomePresence) { + int BLErssi = HomePresence["rssi"]; + Log.trace(F("BLErssi %d" CR), BLErssi); + int txPower = HomePresence["txpower"] | 0; + if (txPower >= 0) + txPower = -59; //if tx power is not found we set a default calibration value + Log.trace(F("TxPower: %d" CR), txPower); + double ratio = BLErssi * 1.0 / txPower; + double distance; + if (ratio < 1.0) { + distance = pow(ratio, 10); + } else { + distance = (0.89976) * pow(ratio, 7.7095) + 0.111; + } + HomePresence["distance"] = distance; + Log.trace(F("Ble distance %D" CR), distance); +} + +void BTforceScan() { + if (!BTProcessLock) { + BLEscan(); + Log.trace(F("Scan done" CR)); + if (BTConfig.bleConnect) + BLEconnect(); + } else { + Log.trace(F("Cannot launch scan due to other process running" CR)); + } +} + +void immediateBTAction(void* pvParameters) { + if (BLEactions.size()) { + // Immediate action; we need to prevent the normal connection action and stop scanning + BTProcessLock = true; + NimBLEScan* pScan = NimBLEDevice::getScan(); + if (pScan->isScanning()) { + pScan->stop(); + } + + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { + // swap the vectors so only this device is processed + std::vector dev_swap; + dev_swap.push_back(getDeviceByMac(BLEactions.back().addr.toString().c_str())); + std::swap(devices, dev_swap); + + std::vector act_swap; + act_swap.push_back(BLEactions.back()); + BLEactions.pop_back(); + std::swap(BLEactions, act_swap); + + // Unlock here to allow the action to be performed + BTProcessLock = false; + BLEconnect(); + // back to normal + std::swap(devices, dev_swap); + std::swap(BLEactions, act_swap); + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + } else { + Log.error(F("CreateOrUpdate Semaphore NOT taken" CR)); + } + + // If we stopped the scheduled connect for this action, do the scheduled now + if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { + timeBetweenConnect = millis(); + BLEconnect(); + } + xSemaphoreGive(semaphoreBLEOperation); + } else { + Log.error(F("BLE busy - immediateBTAction not sent" CR)); + gatewayState = GatewayState::ERROR; + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = BLEactions.back().addr.toString(); + BLEdata["success"] = false; + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + BLEactions.pop_back(); + BTProcessLock = false; + } + } + vTaskDelete(NULL); +} + +void startBTActionTask() { + TaskHandle_t th; + xTaskCreateUniversal( + immediateBTAction, /* Function to implement the task */ + "imActTask", /* Name of the task */ + 8000, /* Stack size in bytes */ + NULL, /* Task input parameter */ + 3, /* Priority of the task (set higher than core task) */ + &th, /* Task handle. */ + 1); /* Core where the task should run */ +} + +# if BLEDecoder +void KnownBTActions(JsonObject& BTdata) { + if (!BTdata.containsKey("id")) { + Log.error(F("BLE mac address missing" CR)); + gatewayState = GatewayState::ERROR; + return; + } + + BLEAction action{}; + action.write = true; + action.ttl = 3; + bool res = false; + if (BTdata.containsKey("model_id") && BTdata["model_id"].is()) { + if (BTdata["model_id"] == "X1") { + if (BTdata.containsKey("cmd") && BTdata["cmd"].is()) { + action.value_type = BLE_VAL_STRING; + std::string val = BTdata["cmd"].as(); // Fix #1694 + action.value = val; + createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBS1, 1); + res = true; + } + } else if (BTdata["model_id"] == "W270160X") { + if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { + action.value_type = BLE_VAL_INT; + res = true; + } else if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { + action.value_type = BLE_VAL_STRING; + res = true; + } + if (res) { + std::string val = BTdata["tilt"].as(); // Fix #1694 + action.value = val; + createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBBT, 1); + } + } else if (BTdata["model_id"] == "W070160X") { + if (BTdata.containsKey("position") && BTdata["position"].is()) { + action.value_type = BLE_VAL_INT; + res = true; + } else if (BTdata.containsKey("position") && BTdata["position"].is()) { + action.value_type = BLE_VAL_STRING; + res = true; + } + if (res) { + std::string val = BTdata["position"].as(); // Fix #1694 + action.value = val; + createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBCU, 1); + } + } + if (res) { + action.addr = NimBLEAddress(BTdata["id"].as(), 1); + BLEactions.push_back(action); + startBTActionTask(); + } else { + Log.error(F("BLE action not recognized" CR)); + gatewayState = GatewayState::ERROR; + } + } +} +# else +void KnownBTActions(JsonObject& BTdata) {} +# endif + +void XtoBTAction(JsonObject& BTdata) { + BLEAction action{}; + action.ttl = BTdata.containsKey("ttl") ? (uint8_t)BTdata["ttl"] : 1; + action.value_type = BLE_VAL_STRING; + if (BTdata.containsKey("value_type")) { + String vt = BTdata["value_type"]; + vt.toUpperCase(); + if (vt == "HEX") + action.value_type = BLE_VAL_HEX; + else if (vt == "INT") + action.value_type = BLE_VAL_INT; + else if (vt == "FLOAT") + action.value_type = BLE_VAL_FLOAT; + else if (vt != "STRING") { + Log.error(F("BLE value type invalid %s" CR), vt.c_str()); + return; + } + } + + Log.trace(F("BLE ACTION TTL = %u" CR), action.ttl); + action.complete = false; + if (BTdata.containsKey("ble_write_address") && + BTdata.containsKey("ble_write_service") && + BTdata.containsKey("ble_write_char") && + BTdata.containsKey("ble_write_value")) { + action.addr = NimBLEAddress(BTdata["ble_write_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); + action.service = NimBLEUUID((const char*)BTdata["ble_write_service"]); + action.characteristic = NimBLEUUID((const char*)BTdata["ble_write_char"]); + std::string val = BTdata["ble_write_value"].as(); // Fix #1694 + action.value = val; + action.write = true; + Log.trace(F("BLE ACTION Write" CR)); + } else if (BTdata.containsKey("ble_read_address") && + BTdata.containsKey("ble_read_service") && + BTdata.containsKey("ble_read_char")) { + action.addr = NimBLEAddress(BTdata["ble_read_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); + action.service = NimBLEUUID((const char*)BTdata["ble_read_service"]); + action.characteristic = NimBLEUUID((const char*)BTdata["ble_read_char"]); + action.write = false; + Log.trace(F("BLE ACTION Read" CR)); + } else { + return; + } + + createOrUpdateDevice(action.addr.toString().c_str(), device_flags_connect, UNKWNON_MODEL, action.addr.getType()); + + BLEactions.push_back(action); + if (BTdata.containsKey("immediate") && BTdata["immediate"].as()) { + startBTActionTask(); + } +} + +void XtoBT(const char* topicOri, JsonObject& BTdata) { // json object decoding + if (cmpToMainTopic(topicOri, subjectMQTTtoBTset)) { + Log.trace(F("MQTTtoBT json set" CR)); + + // Black list & white list set + bool WorBupdated; + WorBupdated = updateWorB(BTdata, true); + WorBupdated |= updateWorB(BTdata, false); + + if (WorBupdated) { + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { + //dumpDevices(); + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + } + } + + // Force scan now + if (BTdata.containsKey("interval") && BTdata["interval"] == 0) { + Log.notice(F("BLE forced scan" CR)); + atomic_store_explicit(&forceBTScan, 1, ::memory_order_seq_cst); // ask the other core to do the scan for us + } + + /* + * Configuration modifications priorities: + * First `init=true` and `load=true` commands are executed (if both are present, INIT prevails on LOAD) + * Then parameters included in json are taken in account + * Finally `erase=true` and `save=true` commands are executed (if both are present, ERASE prevails on SAVE) + */ + if (BTdata.containsKey("init") && BTdata["init"].as()) { + // Restore the default (initial) configuration + BTConfig_init(); + } else if (BTdata.containsKey("load") && BTdata["load"].as()) { + // Load the saved configuration, if not initialised + BTConfig_load(); + } + + // Load config from json if available + BTConfig_fromJson(BTdata); + + } else if (cmpToMainTopic(topicOri, subjectMQTTtoBT)) { + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { + KnownBTActions(BTdata); + XtoBTAction(BTdata); + xSemaphoreGive(semaphoreBLEOperation); + } else { + Log.error(F("BLE busy - BTActions not sent" CR)); + gatewayState = GatewayState::ERROR; + } + } else if (strstr(topicOri, subjectTrackerSync) != NULL) { + if (BTdata.containsKey("gatewayid") && BTdata.containsKey("trackerid") && BTdata["gatewayid"] != gateway_name) { + BLEdevice* device = getDeviceByMac(BTdata["trackerid"].as()); + if (device != &NO_BT_DEVICE_FOUND && device->lastUpdate != 0) { + device->lastUpdate = 0; + Log.notice(F("Tracker %s disassociated by gateway %s" CR), BTdata["trackerid"].as(), BTdata["gatewayid"].as()); + } + } + } +} +#endif From d71cef57bcf7c8cef1ac715e8a96ea80aa366e6b Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sat, 9 Aug 2025 22:57:37 +1200 Subject: [PATCH 14/24] Update docs for displayDeviceName --- docs/use/displays.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/use/displays.md b/docs/use/displays.md index 3e1c1f8e81..4b37aa8d31 100644 --- a/docs/use/displays.md +++ b/docs/use/displays.md @@ -55,7 +55,7 @@ As the display Metric setting is being defined in the WebUI part of OpenMQTTGate `mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoWebUI/config -m {"displayMetric":false}` ### Display name as advetised Bluetooth name or `model_id` -There is a build property of ForceDeviceName which forces devices when they are added in Home Assistant auto-discovery to be created with their Bluetooth advertised name isntead of their `model_id`. The default naming is `model_id` with `{"displayDeviceName":true}`. +There is a build property of ForceDeviceName which forces devices when they are added in Home Assistant auto-discovery to be created with their Bluetooth advertised name isntead of their `model_id`. The default naming is `model_id` with `{"displayDeviceName":true}`. If you have enabled auto-discovery then a restart is required. This can also be adjusted in the WebUI by switching the Configure WebUI Device naming between `Model ID` (false) or `Device name` (true) @@ -63,7 +63,6 @@ This can also be changed with the runtime command. `mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoWebUI/config -m {"displayDeviceName":true}` - ### Rotating the display by 180 degrees This can be set with the compiler directive `-DDISPLAY_FLIP=false`. From 94dda15a1a75c794723cf4873969e2ac96e9d320 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 10 Aug 2025 15:21:27 +1200 Subject: [PATCH 15/24] Add support to properly decode LYWSD03MMC/MJWSD05MMC, also adding RSSI --- main/config_mqttDiscovery.h | 1 + main/gatewayBT.cpp | 26 +++++++++++++++++++++++++- main/mqttDiscovery.cpp | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/main/config_mqttDiscovery.h b/main/config_mqttDiscovery.h index 7cb89e22cd..0305f2e6ce 100644 --- a/main/config_mqttDiscovery.h +++ b/main/config_mqttDiscovery.h @@ -170,6 +170,7 @@ extern char discovery_prefix[]; #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 jsonRSSI "{{ value_json.rssi | is_defined }}" #define stateClassNone "" #define stateClassMeasurement "measurement" diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index 99a50771d1..d4d5a07481 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -579,6 +579,27 @@ void XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel) { createDiscoveryFromList(mac, XMWSDJ04MMCsensor, XMWSDJ04MMCparametersCount, "XMWSDJ04MMC", "Xiaomi", sensorModel); } +void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) { + Log.trace(F("xxWSD0xMMCDiscovery" CR)); + int xxWSD0xMMCparametersCount = 5; + if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) xxWSD0xMMCparametersCount = 7; + const char* xxWSD0xMMCsensor[xxWSD0xMMCparametersCount][9] = { + {"sensor", "Battery", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "Voltage", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "Temperature", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "Humidity", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement}, + {"sensor", "RSSI", mac, "signal_strength", jsonRSSI, "", "", "dB", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement, state class + }; + if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) { + const char* power[9] = {"sensor", "Power", mac, "", jsonPower, "", "", "", stateClassNone}; + memcpy(xxWSD0xMMCsensor[4], power, 9); + const char* open[9] = {"sensor", "Opening", mac, "", jsonOpen, "", "", "", stateClassNone}; + memcpy(xxWSD0xMMCsensor[5], open, 9); + } + createDiscoveryFromList(mac, xxWSD0xMMCsensor, xxWSD0xMMCparametersCount, name, "Xiaomi", sensorModel); +} + # else void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) {} void MHO_C401Discovery(const char* mac, const char* sensorModel) {} @@ -586,6 +607,7 @@ 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 XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel_id) {} +void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel_id) {} # endif /* @@ -996,7 +1018,9 @@ void launchBTDiscovery(bool overrideDiscovery) { model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, stateClassNone); } - if (!properties.empty()) { + if (p->sensorModel_id >= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_ATC && p->sensorModel_id <= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_PVVX_BTHOME_2 ) { + xxWSD0xMMCDiscovery(macWOdots.c_str(), p->name, model_id.c_str()); + } else if (!properties.empty()) { StaticJsonDocument jsonBuffer; auto error = deserializeJson(jsonBuffer, properties); if (error) { diff --git a/main/mqttDiscovery.cpp b/main/mqttDiscovery.cpp index 6bcd1faa60..d60d2be81a 100644 --- a/main/mqttDiscovery.cpp +++ b/main/mqttDiscovery.cpp @@ -586,7 +586,7 @@ void createDiscovery(const char* sensor_type, // generate unique device name by adding the second half of the device_id only if device_name and device_id are different and we don't want to use the BLE name if (device_name[0]) { - #if defined(ZgatewayBT) // displayDeviceName only applies when running with the WebUI and ESP32 + #if defined(ZgatewayBT) // displayDeviceName only applies when running Bluetooth if (strcmp(device_id, device_name) != 0 && device_id[0] && !displayDeviceName) { #else !ForceDeviceName // Support ForceDeviceName for esp8266's if (strcmp(device_id, device_name) != 0 && device_id[0] && !ForceDeviceName) { From b838e1b3d2b25a768666377bb8caaef216aba08b Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 10 Aug 2025 15:36:54 +1200 Subject: [PATCH 16/24] Disable stateBTMeasures(false) --- main/gatewayBT.cpp | 3725 ++++++++++++++++++++++---------------------- 1 file changed, 1863 insertions(+), 1862 deletions(-) diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index d4d5a07481..f2a6fc83b8 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -1,1862 +1,1863 @@ -/* - OpenMQTTGateway - ESP8266 or Arduino program for home automation - - Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker - Send and receiving command by MQTT - - This gateway enables to: - - publish MQTT data to a topic related to BLE devices data - - Copyright: (c)Florian ROBERT - - This file is part of OpenMQTTGateway. - - OpenMQTTGateway is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - OpenMQTTGateway is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Thanks to wolass https://github.com/wolass for suggesting me HM 10 and dinosd https://github.com/dinosd/BLE_PROXIMITY for inspiring me how to implement the gateway -*/ -#include "User_config.h" - -#ifdef ZgatewayBT -# include "TheengsCommon.h" - -SemaphoreHandle_t semaphoreCreateOrUpdateDevice; -SemaphoreHandle_t semaphoreBLEOperation; -QueueHandle_t BLEQueue; -unsigned long scanCount = 0; -# include -# include -# include -# include -# include -# include - -# include - -# include "TheengsCommon.h" -# include "config_mqttDiscovery.h" -# include "gatewayBLEConnect.h" -# include "soc/timer_group_reg.h" -# include "soc/timer_group_struct.h" - -using namespace std; - -// Global struct to store live BT configuration data -BTConfig_s BTConfig; - -# if BLEDecoder -# include -# if BLEDecryptor -# include "mbedtls/ccm.h" -# include "mbedtls/aes.h" -# endif -TheengsDecoder decoder; -# endif - -static TaskHandle_t xCoreTaskHandle; -static TaskHandle_t xProcBLETaskHandle; - -struct decompose { - int start; - int len; - bool reverse; -}; - -vector BLEactions; - -vector devices; -int newDevices = 0; - -static BLEdevice NO_BT_DEVICE_FOUND = { - {0}, - 0, - false, - false, - false, - false, - (char)UNKWNON_MODEL, - 0, -}; -static bool oneWhite = false; - -extern bool BTProcessLock; -extern int queueLength; - -void setupBTTasksAndBLE(); -bool checkIfIsTracker(char ch); -void hass_presence(JsonObject& HomePresence); -void BTforceScan(); - -void BTConfig_init() { - BTConfig.bleConnect = AttemptBLEConnect; - BTConfig.BLEinterval = TimeBtwRead; - BTConfig.adaptiveScan = AdaptiveBLEScan; - BTConfig.intervalActiveScan = TimeBtwActive; - BTConfig.intervalConnect = TimeBtwConnect; - BTConfig.scanDuration = Scan_duration; - BTConfig.pubOnlySensors = PublishOnlySensors; - BTConfig.pubRandomMACs = PublishRandomMACs; - BTConfig.presenceEnable = HassPresence; - BTConfig.presenceTopic = subjectHomePresence; - BTConfig.presenceUseBeaconUuid = useBeaconUuidForPresence; - BTConfig.minRssi = MinimumRSSI; - BTConfig.extDecoderEnable = UseExtDecoder; - BTConfig.extDecoderTopic = MQTTDecodeTopic; - BTConfig.filterConnectable = BLE_FILTER_CONNECTABLE; - BTConfig.pubAdvData = pubBLEAdvData; - BTConfig.pubBeaconUuidForTopic = useBeaconUuidForTopic; - BTConfig.ignoreWBlist = false; - BTConfig.presenceAwayTimer = PresenceAwayTimer; - BTConfig.movingTimer = MovingTimer; - BTConfig.forcePassiveScan = false; - BTConfig.enabled = EnableBT; -} - -unsigned long timeBetweenConnect = 0; -unsigned long timeBetweenActive = 0; - -String stateBTMeasures(bool start) { - StaticJsonDocument jsonBuffer; - JsonObject jo = jsonBuffer.to(); - jo["bleconnect"] = BTConfig.bleConnect; - jo["interval"] = BTConfig.BLEinterval; - jo["adaptivescan"] = BTConfig.adaptiveScan; - jo["intervalacts"] = BTConfig.intervalActiveScan; - jo["intervalcnct"] = BTConfig.intervalConnect; - jo["scanduration"] = BTConfig.scanDuration; - jo["hasspresence"] = BTConfig.presenceEnable; - jo["prestopic"] = BTConfig.presenceTopic; - jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; - jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value - jo["extDecoderEnable"] = BTConfig.extDecoderEnable; - jo["extDecoderTopic"] = BTConfig.extDecoderTopic; - jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; - jo["ignoreWBlist"] = BTConfig.ignoreWBlist; - jo["forcepscn"] = BTConfig.forcePassiveScan; - jo["tskstck"] = uxTaskGetStackHighWaterMark(xProcBLETaskHandle); - jo["crstck"] = uxTaskGetStackHighWaterMark(xCoreTaskHandle); - jo["enabled"] = BTConfig.enabled; - jo["scnct"] = scanCount; -# if BLEDecoder - jo["onlysensors"] = BTConfig.pubOnlySensors; - jo["randommacs"] = BTConfig.pubRandomMACs; - jo["filterConnectable"] = BTConfig.filterConnectable; - jo["pubadvdata"] = BTConfig.pubAdvData; - jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; - jo["movingtimer"] = BTConfig.movingTimer; -# endif - - if (start) { - Log.notice(F("BT sys: ")); - serializeJsonPretty(jsonBuffer, Serial); - Serial.println(); - return ""; // Do not try to erase/write/send config at startup - } - String output; - serializeJson(jo, output); - jo["origin"] = subjectBTtoMQTT; - enqueueJsonObject(jo, QueueSemaphoreTimeOutTask); - return (output); -} - -void BTConfig_fromJson(JsonObject& BTdata, bool startup = false) { - // Attempts to connect to eligible devices or not - Config_update(BTdata, "bleconnect", BTConfig.bleConnect); - // Identify AdaptiveScan deactivation to pass to continuous mode or activation to come back to default settings - if (startup == false) { - if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == false && BTConfig.presenceEnable == true) { - BTdata["adaptivescan"] = true; - } else if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == true && BTConfig.presenceEnable == false) { - BTdata["adaptivescan"] = false; - } - - if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == false && BTConfig.adaptiveScan == true) { - BTdata["interval"] = MinTimeBtwScan; - BTdata["intervalacts"] = MinTimeBtwScan; - BTdata["scanduration"] = MinScanDuration; - } else if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == true && BTConfig.adaptiveScan == false) { - BTdata["interval"] = TimeBtwRead; - BTdata["intervalacts"] = TimeBtwActive; - BTdata["scanduration"] = Scan_duration; - } - // Identify if the gateway is enabled or not and stop start accordingly - if (BTdata.containsKey("enabled") && BTdata["enabled"] == false && BTConfig.enabled == true) { - // Stop the gateway but without deinit to enable a future BT restart - stopProcessing(false); - } else if (BTdata.containsKey("enabled") && BTdata["enabled"] == true && BTConfig.enabled == false) { - BTProcessLock = false; - setupBTTasksAndBLE(); - } - } - // Home Assistant presence message - Config_update(BTdata, "hasspresence", BTConfig.presenceEnable); - // Time before before active scan - // Scan interval set - and avoid intervalacts to be lower than interval - if (BTdata.containsKey("interval") && BTdata["interval"] != 0) { - BTConfig.adaptiveScan = false; - Config_update(BTdata, "interval", BTConfig.BLEinterval); - if (BTConfig.intervalActiveScan < BTConfig.BLEinterval) { - Config_update(BTdata, "interval", BTConfig.intervalActiveScan); - } - } - // Define if the scan is adaptive or not - and avoid intervalacts to be lower than interval - if (BTdata.containsKey("intervalacts") && BTdata["intervalacts"] < BTConfig.BLEinterval) { - BTConfig.adaptiveScan = false; - // Config_update(BTdata, "interval", BTConfig.intervalActiveScan); - BTConfig.intervalActiveScan = BTConfig.BLEinterval; - } else { - Config_update(BTdata, "intervalacts", BTConfig.intervalActiveScan); - } - // Adaptive scan set - Config_update(BTdata, "adaptivescan", BTConfig.adaptiveScan); - // Time before a connect set - Config_update(BTdata, "intervalcnct", BTConfig.intervalConnect); - // publish all BLE devices discovered or only the identified sensors (like temperature sensors) - Config_update(BTdata, "scanduration", BTConfig.scanDuration); - // define the duration for a scan; in milliseconds - Config_update(BTdata, "onlysensors", BTConfig.pubOnlySensors); - // publish devices which randomly change their MAC addresses - Config_update(BTdata, "randommacs", BTConfig.pubRandomMACs); - // Home Assistant presence message topic - Config_update(BTdata, "prestopic", BTConfig.presenceTopic); - // Home Assistant presence message use iBeacon UUID - Config_update(BTdata, "presuseuuid", BTConfig.presenceUseBeaconUuid); - // Timer to trigger a device state as offline if not seen - Config_update(BTdata, "presenceawaytimer", BTConfig.presenceAwayTimer); - // Timer to trigger a device state as offline if not seen - Config_update(BTdata, "movingtimer", BTConfig.movingTimer); - // Force passive scan - Config_update(BTdata, "forcepscn", BTConfig.forcePassiveScan); - // MinRSSI set - Config_update(BTdata, "minrssi", BTConfig.minRssi); - // Send undecoded device data - Config_update(BTdata, "extDecoderEnable", BTConfig.extDecoderEnable); - // Topic to send undecoded device data - Config_update(BTdata, "extDecoderTopic", BTConfig.extDecoderTopic); - // Sets whether to filter publishing - Config_update(BTdata, "filterConnectable", BTConfig.filterConnectable); - // Publish advertisement data - Config_update(BTdata, "pubadvdata", BTConfig.pubAdvData); - // Use iBeacon UUID as topic, instead of sender (random) MAC address - Config_update(BTdata, "pubuuid4topic", BTConfig.pubBeaconUuidForTopic); - // Disable Whitelist & Blacklist - Config_update(BTdata, "ignoreWBlist", (BTConfig.ignoreWBlist)); - // Enable or disable the BT gateway - Config_update(BTdata, "enabled", BTConfig.enabled); - - stateBTMeasures(startup); - - if (BTdata.containsKey("erase") && BTdata["erase"].as()) { - // Erase config from NVS (non-volatile storage) - preferences.begin(Gateway_Short_Name, false); - if (preferences.isKey("BTConfig")) { - int result = preferences.remove("BTConfig"); - Log.notice(F("BT config erase result: %d" CR), result); - preferences.end(); - return; // Erase prevails on save, so skipping save - } else { - preferences.end(); - Log.notice(F("BT config not found" CR)); - } - } - - if (BTdata.containsKey("save") && BTdata["save"].as()) { - StaticJsonDocument jsonBuffer; - JsonObject jo = jsonBuffer.to(); - jo["bleconnect"] = BTConfig.bleConnect; - jo["interval"] = BTConfig.BLEinterval; - jo["adaptivescan"] = BTConfig.adaptiveScan; - jo["intervalacts"] = BTConfig.intervalActiveScan; - jo["intervalcnct"] = BTConfig.intervalConnect; - jo["scanduration"] = BTConfig.scanDuration; - jo["onlysensors"] = BTConfig.pubOnlySensors; - jo["randommacs"] = BTConfig.pubRandomMACs; - jo["hasspresence"] = BTConfig.presenceEnable; - jo["prestopic"] = BTConfig.presenceTopic; - jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; - jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value - jo["extDecoderEnable"] = BTConfig.extDecoderEnable; - jo["extDecoderTopic"] = BTConfig.extDecoderTopic; - jo["filterConnectable"] = BTConfig.filterConnectable; - jo["pubadvdata"] = BTConfig.pubAdvData; - jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; - jo["ignoreWBlist"] = BTConfig.ignoreWBlist; - jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; - jo["movingtimer"] = BTConfig.movingTimer; - jo["forcepscn"] = BTConfig.forcePassiveScan; - jo["enabled"] = BTConfig.enabled; - // Save config into NVS (non-volatile storage) - String conf = ""; - serializeJson(jsonBuffer, conf); - preferences.begin(Gateway_Short_Name, false); - int result = preferences.putString("BTConfig", conf); - preferences.end(); - Log.notice(F("BT config save: %s, result: %d" CR), conf.c_str(), result); - } -} - -void BTConfig_load() { - StaticJsonDocument jsonBuffer; - preferences.begin(Gateway_Short_Name, true); - if (preferences.isKey("BTConfig")) { - auto error = deserializeJson(jsonBuffer, preferences.getString("BTConfig", "{}")); - preferences.end(); - Log.notice(F("BT config loaded" CR)); - if (error) { - Log.error(F("BT config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity()); - return; - } - if (jsonBuffer.isNull()) { - Log.warning(F("BT config is null" CR)); - return; - } - JsonObject jo = jsonBuffer.as(); - BTConfig_fromJson(jo, true); // Never send MQTT message with config - Log.notice(F("BT config loaded" CR)); - } else { - preferences.end(); - Log.notice(F("BT config not found" CR)); - } -} - -void PublishDeviceData(JsonObject& BLEdata); - -atomic_int forceBTScan; - -void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type = 0, const char* name = ""); - -BLEdevice* getDeviceByMac(const char* mac); // Declared here to avoid pre-compilation issue (misplaced auto declaration by pio) -BLEdevice* getDeviceByMac(const char* mac) { - Log.trace(F("getDeviceByMac %s" CR), mac); - - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - if ((strcmp((*it)->macAdr, mac) == 0)) { - return *it; - } - } - return &NO_BT_DEVICE_FOUND; -} - -bool updateWorB(JsonObject& BTdata, bool isWhite) { - Log.trace(F("update WorB" CR)); - const char* jsonKey = isWhite ? "white-list" : "black-list"; - - int size = BTdata[jsonKey].size(); - if (size == 0) - return false; - - for (int i = 0; i < size; i++) { - const char* mac = BTdata[jsonKey][i]; - createOrUpdateDevice(mac, (isWhite ? device_flags_isWhiteL : device_flags_isBlackL), - UNKWNON_MODEL); - } - - return true; -} - -void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type, const char* name) { - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(30000)) == pdFALSE) { - Log.error(F("Semaphore NOT taken" CR)); - return; - } - BLEdevice* device = getDeviceByMac(mac); - if (device == &NO_BT_DEVICE_FOUND) { - Log.trace(F("add %s" CR), mac); - //new device - device = new BLEdevice(); - strcpy(device->macAdr, mac); - device->isDisc = flags & device_flags_isDisc; - device->isWhtL = flags & device_flags_isWhiteL; - device->isBlkL = flags & device_flags_isBlackL; - device->connect = flags & device_flags_connect; - device->macType = mac_type; - // Check name length - if (strlen(name) > 20) { - Log.warning(F("Name too long, truncating" CR)); - strncpy(device->name, name, 20); - device->name[19] = '\0'; - } else { - strcpy(device->name, name); - } - device->sensorModel_id = model; - device->lastUpdate = millis(); - devices.push_back(device); - newDevices++; - } else { - Log.trace(F("update %s" CR), mac); - device->lastUpdate = millis(); - device->macType = mac_type; - - if (flags & device_flags_isDisc) { - device->isDisc = true; - } - - if (flags & device_flags_connect) { - device->connect = true; - } - - if (model != UNKWNON_MODEL && device->sensorModel_id == UNKWNON_MODEL) { - newDevices++; - device->isDisc = false; - device->sensorModel_id = model; - } - - // If a device has been added to the white-list, flag it so it can be auto-detected - if (!device->isWhtL && flags & device_flags_isWhiteL) { - newDevices++; - } - if (flags & device_flags_isWhiteL || flags & device_flags_isBlackL) { - device->isWhtL = flags & device_flags_isWhiteL; - device->isBlkL = flags & device_flags_isBlackL; - } - } - - // update oneWhite flag - oneWhite = oneWhite || device->isWhtL; - - xSemaphoreGive(semaphoreCreateOrUpdateDevice); -} - -void updateDevicesStatus() { - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - BLEdevice* p = *it; - unsigned long now = millis(); - // Check for tracker status - bool isTracker = false; -# if BLEDecoder - std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); - if (tag.length() >= 4) { - isTracker = checkIfIsTracker(tag[3]); - } - // Device tracker devices - if (isTracker) { // We apply the offline status only for tracking device, can be extended further to all the devices - if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.presenceAwayTimer) && (now > BTConfig.presenceAwayTimer)) && - (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = p->macAdr; - BLEdata["state"] = "offline"; - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - // We set the lastUpdate to 0 to avoid replublishing the offline state - p->lastUpdate = 0; - } - } - // Moving detection devices (devices with an accelerometer) - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BC08) { - if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.movingTimer) && (now > BTConfig.movingTimer)) && - (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = p->macAdr; - BLEdata["state"] = "offline"; - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - // We set the lastUpdate to 0 to avoid replublishing the offline state - p->lastUpdate = 0; - } - } -# endif - } -} - -void dumpDevices() { -# if LOG_LEVEL > LOG_LEVEL_NOTICE - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - BLEdevice* p = *it; - Log.trace(F("macAdr %s" CR), p->macAdr); - Log.trace(F("macType %d" CR), p->macType); - Log.trace(F("isDisc %d" CR), p->isDisc); - Log.trace(F("isWhtL %d" CR), p->isWhtL); - Log.trace(F("isBlkL %d" CR), p->isBlkL); - Log.trace(F("connect %d" CR), p->connect); - Log.trace(F("sensorModel_id %d" CR), p->sensorModel_id); - Log.trace(F("LastUpdate %u" CR), p->lastUpdate); - } -# endif -} - -void strupp(char* beg) { - while ((*beg = toupper(*beg))) - ++beg; -} - -# ifdef ZmqttDiscovery -void DT24Discovery(const char* mac, const char* sensorModel_id) { -# define DT24parametersCount 7 - Log.trace(F("DT24Discovery" CR)); - const char* DT24sensor[DT24parametersCount][9] = { - {"sensor", "volt", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "amp", mac, "current", jsonCurrent, "", "", "A", stateClassMeasurement}, - {"sensor", "watt", mac, "power", jsonPower, "", "", "W", stateClassMeasurement}, - {"sensor", "watt-hour", mac, "power", jsonEnergy, "", "", "kWh", stateClassMeasurement}, - {"sensor", "price", mac, "", jsonMsg, "", "", "", stateClassNone}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"binary_sensor", "inUse", mac, "power", jsonInuse, "", "", "", stateClassNone} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, DT24sensor, DT24parametersCount, "DT24", "ATorch", sensorModel_id); -} - -void BM2Discovery(const char* mac, const char* sensorModel_id) { -# define BM2parametersCount 2 - Log.trace(F("BM2Discovery" CR)); - const char* BM2sensor[BM2parametersCount][9] = { - {"sensor", "volt", mac, "voltage", jsonVoltBM2, "", "", "V", stateClassMeasurement}, // We use a json definition that retrieve only data from the BM2 decoder, as this sensor also advertize volt as an iBeacon - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, BM2sensor, BM2parametersCount, "BM2", "Generic", sensorModel_id); -} - -void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) { -# define LYWSD03MMCparametersCount 4 - Log.trace(F("LYWSD03MMCDiscovery" CR)); - const char* LYWSD03MMCsensor[LYWSD03MMCparametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, LYWSD03MMCsensor, LYWSD03MMCparametersCount, "LYWSD03MMC", "Xiaomi", sensorModel); -} - -void MHO_C401Discovery(const char* mac, const char* sensorModel) { -# define MHO_C401parametersCount 4 - Log.trace(F("MHO_C401Discovery" CR)); - const char* MHO_C401sensor[MHO_C401parametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, MHO_C401sensor, MHO_C401parametersCount, "MHO_C401", "Xiaomi", sensorModel); -} - -void HHCCJCY01HHCCDiscovery(const char* mac, const char* sensorModel) { -# define HHCCJCY01HHCCparametersCount 5 - Log.trace(F("HHCCJCY01HHCCDiscovery" CR)); - const char* HHCCJCY01HHCCsensor[HHCCJCY01HHCCparametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "lux", mac, "illuminance", jsonLux, "", "", "lx", stateClassMeasurement}, - {"sensor", "fer", mac, "", jsonFer, "", "", "µS/cm", stateClassMeasurement}, - {"sensor", "moi", mac, "", jsonMoi, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, HHCCJCY01HHCCsensor, HHCCJCY01HHCCparametersCount, "HHCCJCY01HHCC", "Xiaomi", sensorModel); -} - -void XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel) { -# define XMWSDJ04MMCparametersCount 4 - Log.trace(F("XMWSDJ04MMCDiscovery" CR)); - const char* XMWSDJ04MMCsensor[XMWSDJ04MMCparametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, XMWSDJ04MMCsensor, XMWSDJ04MMCparametersCount, "XMWSDJ04MMC", "Xiaomi", sensorModel); -} - -void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) { - Log.trace(F("xxWSD0xMMCDiscovery" CR)); - int xxWSD0xMMCparametersCount = 5; - if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) xxWSD0xMMCparametersCount = 7; - const char* xxWSD0xMMCsensor[xxWSD0xMMCparametersCount][9] = { - {"sensor", "Battery", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "Voltage", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "Temperature", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "Humidity", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement}, - {"sensor", "RSSI", mac, "signal_strength", jsonRSSI, "", "", "dB", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement, state class - }; - if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) { - const char* power[9] = {"sensor", "Power", mac, "", jsonPower, "", "", "", stateClassNone}; - memcpy(xxWSD0xMMCsensor[4], power, 9); - const char* open[9] = {"sensor", "Opening", mac, "", jsonOpen, "", "", "", stateClassNone}; - memcpy(xxWSD0xMMCsensor[5], open, 9); - } - createDiscoveryFromList(mac, xxWSD0xMMCsensor, xxWSD0xMMCparametersCount, name, "Xiaomi", sensorModel); -} - -# else -void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) {} -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 XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel_id) {} -void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel_id) {} -# endif - -/* - Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleScan.cpp - Ported to Arduino ESP32 by Evandro Copercini - */ -// core task implementation thanks to https://techtutorialsx.com/2017/05/09/esp32-running-code-on-a-specific-core/ - -//core on which the BLE detection task will run -static int taskCore = 0; - -class ScanCallbacks : public NimBLEScanCallbacks { - void onResult(const NimBLEAdvertisedDevice* advertisedDevice) { - NimBLEAdvertisedDevice* ad = new NimBLEAdvertisedDevice(*advertisedDevice); - if (xQueueSend(BLEQueue, &ad, 0) != pdTRUE) { - Log.error(F("BLEQueue full" CR)); - delete (ad); - } - } -} scanCallbacks; - -std::string convertServiceData(std::string deviceServiceData) { - int serviceDataLength = (int)deviceServiceData.length(); - char spr[2 * serviceDataLength + 1]; - for (int i = 0; i < serviceDataLength; i++) sprintf(spr + 2 * i, "%.2x", (unsigned char)deviceServiceData[i]); - spr[2 * serviceDataLength] = 0; - Log.trace(F("Converted service data (%d) to %s" CR), serviceDataLength, spr); - return spr; -} - -bool checkIfIsTracker(char ch) { - uint8_t data = 0; - if (ch >= '0' && ch <= '9') - data = ch - '0'; - else if (ch >= 'a' && ch <= 'f') - data = 10 + (ch - 'a'); - - if (((data >> 3) & 0x01) == 1) { - Log.trace(F("Is Device Tracker" CR)); - return true; - } else { - return false; - } -} - -void procBLETask(void* pvParameters) { - BLEAdvertisedDevice* advertisedDevice = nullptr; - - for (;;) { - xQueueReceive(BLEQueue, &advertisedDevice, portMAX_DELAY); - // Feed the watchdog - //esp_task_wdt_reset(); - if (!BTProcessLock) { - Log.trace(F("Creating BLE buffer" CR)); - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = advertisedDevice->getAddress().toString(); - BLEdata["mac_type"] = advertisedDevice->getAddress().getType(); - BLEdata["adv_type"] = advertisedDevice->getAdvType(); - Log.notice(F("BT Device detected: %s" CR), BLEdata["id"].as()); - BLEdevice* device = getDeviceByMac(BLEdata["id"].as()); - - if (BTConfig.filterConnectable && device->connect) { - Log.notice(F("Filtered connectable device" CR)); - delete (advertisedDevice); - continue; - } - - if (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(device)) && !isBlack(device))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC) - if (advertisedDevice->haveName()) - BLEdata["name"] = (char*)advertisedDevice->getName().c_str(); - if (advertisedDevice->haveManufacturerData()) { - BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString((uint8_t*)advertisedDevice->getManufacturerData().data(), - advertisedDevice->getManufacturerData().length()); - } - BLEdata["rssi"] = (int)advertisedDevice->getRSSI(); - if (advertisedDevice->haveTXPower()) - BLEdata["txpower"] = (int8_t)advertisedDevice->getTXPower(); - if (BTConfig.presenceEnable) { - hass_presence(BLEdata); // with either only sensors or not we can use it for home assistant room presence component - } - if (advertisedDevice->haveServiceData()) { - int serviceDataCount = advertisedDevice->getServiceDataCount(); - Log.trace(F("Get services data number: %d" CR), serviceDataCount); - for (int j = 0; j < serviceDataCount; j++) { - StaticJsonDocument BLEdataBufferTemp; - JsonObject BLEdataTemp = BLEdataBufferTemp.to(); - BLEdataBufferTemp = BLEdataBuffer; - std::string service_data = convertServiceData(advertisedDevice->getServiceData(j)); - Log.trace(F("Service data: %s" CR), service_data.c_str()); - std::string serviceDatauuid = advertisedDevice->getServiceDataUUID(j).toString(); - Log.trace(F("Service data UUID: %s" CR), (char*)serviceDatauuid.c_str()); - BLEdataTemp["servicedata"] = (char*)service_data.c_str(); - BLEdataTemp["servicedatauuid"] = (char*)serviceDatauuid.c_str(); - PublishDeviceData(BLEdataTemp); - } - } else { - PublishDeviceData(BLEdata); - } - } else { - Log.trace(F("Filtered MAC device" CR)); - } - updateDevicesStatus(); - } - delete (advertisedDevice); - vTaskDelay(10); - } -} - -/** - * BLEscan used to retrieve BLE advertized data from devices without connection - */ -void BLEscan() { - // Don't start the next scan until processing of previous results is complete. - while (uxQueueMessagesWaiting(BLEQueue) || queueLength != 0) { // the criteria on queueLength could be adjusted to parallelize the scan and the queue processing - delay(1); // Wait for queue to empty, a yield here instead of the delay cause the WDT to trigger - } - Log.notice(F("Scan begin" CR)); - BLEScan* pBLEScan = BLEDevice::getScan(); - pBLEScan->setScanCallbacks(&scanCallbacks); - if ((millis() > (timeBetweenActive + BTConfig.intervalActiveScan) || BTConfig.intervalActiveScan == BTConfig.BLEinterval) && !BTConfig.forcePassiveScan) { - pBLEScan->setActiveScan(true); - timeBetweenActive = millis(); - } else { - pBLEScan->setActiveScan(false); - } - pBLEScan->setInterval(BLEScanInterval); - pBLEScan->setWindow(BLEScanWindow); - NimBLEScanResults foundDevices = pBLEScan->getResults(BTConfig.scanDuration, false); - if (foundDevices.getCount()) - scanCount++; - Log.notice(F("Found %d devices, scan number %d end" CR), foundDevices.getCount(), scanCount); - Log.trace(F("Process BLE stack free: %u" CR), uxTaskGetStackHighWaterMark(xProcBLETaskHandle)); -} - -/** - * Connect to BLE devices and initiate the callbacks with a service/characteristic request - */ -# if BLEDecoder -void BLEconnect() { - if (!BTProcessLock) { - Log.notice(F("BLE Connect begin" CR)); - do { - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - BLEdevice* p = *it; - if (p->connect) { - Log.trace(F("Model to connect found: %s" CR), p->macAdr); - NimBLEAddress addr((const char*)p->macAdr, p->macType); - if (p->sensorModel_id == BLEconectable::id::LYWSD03MMC || - p->sensorModel_id == BLEconectable::id::MHO_C401) { - LYWSD03MMC_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - BLEclient.publishData(); - } else if (p->sensorModel_id == BLEconectable::id::DT24_BLE) { - DT24_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - BLEclient.publishData(); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { - BM2_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); - BLEclient.publishData(); - } else if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { - XMWSDJ04MMC_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - BLEclient.publishData(); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1) { - SBS1_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT) { - SBBT_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU) { - SBCU_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - } else { - GENERIC_connect BLEclient(addr); - if (BLEclient.processActions(BLEactions)) { - // If we don't regularly connect to this, disable connections so advertisements - // won't be filtered if BLE_FILTER_CONNECTABLE is set. - p->connect = false; - } - } - if (BLEactions.size() > 0) { - std::vector swap; - for (auto& it : BLEactions) { - if (!it.complete && --it.ttl) { - swap.push_back(it); - } else if (it.addr == NimBLEAddress(p->macAdr, p->macType)) { - if (p->sensorModel_id != BLEconectable::id::DT24_BLE && - 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 != BLEconectable::id::MHO_C401 && - p->sensorModel_id != BLEconectable::id::XMWSDJ04MMC) { - // if irregulary connected to and connection failed clear the connect flag. - p->connect = false; - } - } - } - std::swap(BLEactions, swap); - } - } - } - } while (BLEactions.size() > 0); - Log.notice(F("BLE Connect end" CR)); - } -} -# else -void BLEconnect() {} -# endif - -void stopProcessing(bool deinit) { - if (BTConfig.enabled) { - BTProcessLock = true; - // We stop the scan - Log.notice(F("Stopping BLE scan" CR)); - BLEScan* pBLEScan = BLEDevice::getScan(); - if (pBLEScan->isScanning()) { - pBLEScan->stop(); - } - - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { - Log.notice(F("Stopping BLE tasks" CR)); - //Suspending, deleting tasks and stopping BT to free memory - vTaskSuspend(xCoreTaskHandle); - vTaskDelete(xCoreTaskHandle); - vTaskSuspend(xProcBLETaskHandle); - vTaskDelete(xProcBLETaskHandle); - xSemaphoreGive(semaphoreBLEOperation); - } - // Using deinit to free memory, should only be used if we are going to restart the gateway - if (deinit) - BLEDevice::deinit(true); - } - Log.notice(F("BLE gateway stopped, free heap: %d" CR), ESP.getFreeHeap()); -} - -void coreTask(void* pvParameters) { - while (true) { - if (!BTProcessLock) { - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(30000)) == pdTRUE) { - BLEscan(); - // Launching a connect every TimeBtwConnect - if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { - timeBetweenConnect = millis(); - BLEconnect(); - } - //dumpDevices(); - Log.trace(F("CoreTask stack free: %u" CR), uxTaskGetStackHighWaterMark(xCoreTaskHandle)); - xSemaphoreGive(semaphoreBLEOperation); - } else { - Log.error(F("Failed to start scan - BLE busy" CR)); - } - if (SYSConfig.powerMode > 0) { - int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); // is this enough, it will wait the full deepsleep... - if (scan == 1) BTforceScan(); - ready_to_sleep = true; - } else { - for (int interval = BTConfig.BLEinterval, waitms; interval > 0; interval -= waitms) { - int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); - if (scan == 1) BTforceScan(); // should we break after this? - delay(waitms = interval > 100 ? 100 : interval); // 100ms - } - } - } - delay(1); - } -} - -void setupBTTasksAndBLE() { -# ifdef CONFIG_BTDM_BLE_SCAN_DUPL - BLEDevice::setScanDuplicateCacheSize(BLEScanDuplicateCacheSize); -# endif - BLEDevice::init(""); - xTaskCreateUniversal( - procBLETask, /* Function to implement the task */ - "procBLETask", /* Name of the task */ -# if defined(USE_ESP_IDF) || defined(USE_BLUFI) - 14500, -# else - 9500, /* Stack size in bytes */ -# endif - NULL, /* Task input parameter */ - 2, /* Priority of the task (set higher than core task) */ - &xProcBLETaskHandle, /* Task handle. */ - 1); /* Core where the task should run */ - - // we setup a task with priority one to avoid conflict with other gateways - xTaskCreateUniversal( - coreTask, /* Function to implement the task */ - "coreTask", /* Name of the task */ - 5120, /* Stack size in bytes */ - NULL, /* Task input parameter */ - 1, /* Priority of the task */ - &xCoreTaskHandle, /* Task handle. */ - taskCore); /* Core where the task should run */ -} - -void setupBT() { - BTConfig_init(); - BTConfig_load(); - Log.notice(F("BLE scans interval: %d" CR), BTConfig.BLEinterval); - Log.notice(F("BLE connects interval: %d" CR), BTConfig.intervalConnect); - Log.notice(F("BLE scan duration: %d" CR), BTConfig.scanDuration); - Log.notice(F("Publishing only BLE sensors: %T" CR), BTConfig.pubOnlySensors); - Log.notice(F("Publishing random MAC devices: %T" CR), BTConfig.pubRandomMACs); - Log.notice(F("Adaptive BLE scan: %T" CR), BTConfig.adaptiveScan); - Log.notice(F("Active BLE scan interval: %d" CR), BTConfig.intervalActiveScan); - Log.notice(F("minrssi: %d" CR), -abs(BTConfig.minRssi)); - Log.notice(F("Presence Away Timer: %d" CR), BTConfig.presenceAwayTimer); - Log.notice(F("Moving Timer: %d" CR), BTConfig.movingTimer); - Log.notice(F("Force passive scan: %T" CR), BTConfig.forcePassiveScan); - Log.notice(F("Enabled BLE: %T" CR), BTConfig.enabled); - - atomic_init(&forceBTScan, 0); // in theory, we don't need this - - semaphoreCreateOrUpdateDevice = xSemaphoreCreateBinary(); - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - - semaphoreBLEOperation = xSemaphoreCreateBinary(); - xSemaphoreGive(semaphoreBLEOperation); - - BLEQueue = xQueueCreate(QueueSize, sizeof(NimBLEAdvertisedDevice*)); - if (BTConfig.enabled) { - setupBTTasksAndBLE(); - Log.notice(F("gatewayBT multicore ESP32 setup done" CR)); - } else { - Log.notice(F("gatewayBT multicore ESP32 setup disabled" CR)); - } -} - -boolean valid_service_data(const char* data, int size) { - for (int i = 0; i < size; ++i) { - if (data[i] != 48) // 48 correspond to 0 in ASCII table - return true; - } - return false; -} - -# if defined(ZmqttDiscovery) && BLEDecoder == true -// This function always should be called from the main core as it generates direct mqtt messages -// When overrideDiscovery=true, we publish discovery messages of known devices (even if no new) -void launchBTDiscovery(bool overrideDiscovery) { - if (!overrideDiscovery && newDevices == 0) - return; - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdFALSE) { - Log.error(F("Semaphore NOT taken" CR)); - return; - } - newDevices = 0; - vector localDevices = devices; - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - for (vector::iterator it = localDevices.begin(); it != localDevices.end(); ++it) { - BLEdevice* p = *it; - Log.trace(F("Device mac %s" CR), p->macAdr); - // Do not launch discovery for the devices already discovered (unless we have overrideDiscovery) or that are not unique by their MAC Address (iBeacon, GAEN and Microsoft CDP) - if (overrideDiscovery || !isDiscovered(p)) { - String macWOdots = String(p->macAdr); - macWOdots.replace(":", ""); - if (p->sensorModel_id >= 0) { - Log.trace(F("Looking for Model_id: %d" CR), p->sensorModel_id); - std::string properties = decoder.getTheengProperties(p->sensorModel_id); - Log.trace(F("properties: %s" CR), properties.c_str()); - std::string brand = decoder.getTheengAttribute(p->sensorModel_id, "brand"); - std::string model = decoder.getTheengAttribute(p->sensorModel_id, "model"); - if (displayDeviceName || ForceDeviceName) { - if (p->name[0] != '\0') { - model = p->name; - } - } - std::string model_id = decoder.getTheengAttribute(p->sensorModel_id, "model_id"); - - // Check for tracker status - bool isTracker = false; - std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); - if (tag.length() >= 4) { - isTracker = checkIfIsTracker(tag[3]); - } - - String discovery_topic = String(subjectBTtoMQTT) + "/" + macWOdots; - 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 - if (isTracker) { - String tracker_name = String(model_id.c_str()) + "-tracker"; - String tracker_id = macWOdots + "-tracker"; - createDiscovery("device_tracker", - discovery_topic.c_str(), tracker_name.c_str(), 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 == TheengsDecoder::BLE_ID_NUM::BC08) { - String sensor_name = String(model_id.c_str()) + "-moving"; - String sensor_id = macWOdots + "-moving"; - createDiscovery("binary_sensor", - discovery_topic.c_str(), sensor_name.c_str(), sensor_id.c_str(), - will_Topic, "moving", "{% if value_json.get('accx') -%}on{%- else -%}off{%- endif %}", - "on", "off", "", - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } - if (p->sensorModel_id >= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_ATC && p->sensorModel_id <= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_PVVX_BTHOME_2 ) { - xxWSD0xMMCDiscovery(macWOdots.c_str(), p->name, model_id.c_str()); - } else if (!properties.empty()) { - StaticJsonDocument jsonBuffer; - auto error = deserializeJson(jsonBuffer, properties); - if (error) { - if (jsonBuffer.overflowed()) { - // This should not happen if JSON_MSG_BUFFER is large enough for - // the Theengs json properties - Log.error(F("JSON deserialization of Theengs properties overflowed (error %s), buffer capacity: %u. Program might crash. Properties json: %s" CR), - error.c_str(), jsonBuffer.capacity(), properties.c_str()); - } else { - Log.error(F("JSON deserialization of Theengs properties errored: %" CR), - error.c_str()); - } - } - for (JsonPair prop : jsonBuffer["properties"].as()) { - Log.trace(F("Key: %s"), prop.key().c_str()); - Log.trace(F("Unit: %s"), prop.value()["unit"].as()); - Log.trace(F("Name: %s"), prop.value()["name"].as()); - String entity_name = ""; - if (displayDeviceName || ForceDeviceName) { - entity_name = String(model.c_str()) + "-" + String(prop.key().c_str()); - } else { - entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); - } - String unique_id = macWOdots + "-" + String(prop.key().c_str()); - String value_template = "{{ value_json." + String(prop.key().c_str()) + " | is_defined }}"; - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1 && strcmp(prop.key().c_str(), "state") == 0) { - String payload_on = "{\"model_id\":\"X1\",\"cmd\":\"on\",\"id\":\"" + String(p->macAdr) + "\"}"; - String payload_off = "{\"model_id\":\"X1\",\"cmd\":\"off\",\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("switch", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "switch", value_template.c_str(), - payload_on.c_str(), payload_off.c_str(), "", 0, - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone, "off", "on"); - unique_id = macWOdots + "-press"; - entity_name = String(model_id.c_str()) + "-press"; - String payload_press = "{\"model_id\":\"X1\",\"cmd\":\"press\",\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("button", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "button", "", - payload_press.c_str(), "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT && strcmp(prop.key().c_str(), "open") == 0) { - value_template = "{% if value_json.direction == \"up\" -%} {{ 100 - value_json.open/2 }}{% elif value_json.direction == \"down\" %}{{ value_json.open/2 }}{% else %} {{ value_json.open/2 }}{%- endif %}"; - String command_template = "{\"model_id\":\"W270160X\",\"tilt\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("cover", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "cover", value_template.c_str(), - "50", "", "", 0, - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - "blind", nullptr, nullptr, nullptr, command_template.c_str()); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU && strcmp(prop.key().c_str(), "position") == 0) { - String command_template = "{\"model_id\":\"W070160X\",\"position\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("cover", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "cover", "{{ value_json.position }}", - "0", "100", "", 0, - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - "curtain", nullptr, nullptr, nullptr, command_template.c_str()); - } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && - strcmp(prop.key().c_str(), "weighing_mode") == 0) { - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "enum", value_template.c_str(), - "", "", prop.value()["unit"], - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassMeasurement, nullptr, nullptr, "[\"person\",\"object\"]"); - } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && - strcmp(prop.key().c_str(), "unit") == 0) { - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "enum", value_template.c_str(), - "", "", prop.value()["unit"], - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassMeasurement, nullptr, nullptr, "[\"lb\",\"kg\",\"jin\"]"); - } else if (strcmp(prop.value()["unit"], "string") == 0 && strcmp(prop.key().c_str(), "mac") != 0) { - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "", "", "", - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (p->sensorModel_id == TheengsDecoder::MUE4094RT && strcmp(prop.value()["unit"], "status") == 0) { // This device does not a broadcast when there is nothing detected so adding a timeout - createDiscovery("binary_sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "True", "False", "", - BTConfig.presenceAwayTimer / 1000, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (strcmp(prop.value()["unit"], "status") == 0) { - createDiscovery("binary_sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "True", "False", "", - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (strcmp(prop.key().c_str(), "device") != 0 && strcmp(prop.key().c_str(), "mac") != 0) { // Exception on device and mac as these ones are not sensors - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "", "", prop.value()["unit"], - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassMeasurement); - } - } - } - } 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) { - // 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"); - } - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { - // Sensor discovery - BM2Discovery(macWOdots.c_str(), "BM2"); - // Device tracker discovery - String tracker_id = macWOdots + "-tracker"; - createDiscovery("device_tracker", - discovery_topic.c_str(), "BM2-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"); - } - if (p->sensorModel_id == BLEconectable::id::MHO_C401) { - MHO_C401Discovery(macWOdots.c_str(), "MHO-C401"); - } - if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { - XMWSDJ04MMCDiscovery(macWOdots.c_str(), "XMWSDJ04MMC"); - } - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC) { - HHCCJCY01HHCCDiscovery(macWOdots.c_str(), "HHCCJCY01HHCC"); - } - } else { - Log.trace(F("Device UNKNOWN_MODEL %s" CR), p->macAdr); - } - } - } - p->isDisc = true; // we don't need the semaphore and all the search magic via createOrUpdateDevice - } else { - Log.trace(F("Device already discovered or that doesn't require discovery %s" CR), p->macAdr); - } - } -} -# else -void launchBTDiscovery(bool overrideDiscovery) {} -# endif - -# if BLEDecryptor -// ** TODO - Hex string to bytes, there is probably a function for this already just need to find it -int hexToBytes(String hex, uint8_t *out, size_t maxLen) { - int len = hex.length(); - if (len % 2 || len / 2 > maxLen) return -1; - for (int i = 0, j = 0; i < len; i += 2, j++) { - out[j] = (uint8_t) strtol(hex.substring(i, i + 2).c_str(), nullptr, 16); - } - return len / 2; -} -// Reverse bytes -void reverseBytes(uint8_t *data, size_t length) { - size_t i; - for (i = 0; i < length / 2; i++) { - uint8_t temp = data[i]; - data[i] = data[length - 1 - i]; - data[length - 1 - i] = temp; - } -} -# endif - -# if BLEDecoder -void process_bledata(JsonObject& BLEdata) { - yield(); // Necessary to let the loop run in case of connectivity issues - if (!BLEdata.containsKey("id")) { - Log.error(F("No mac address in the payload" CR)); - return; - } - const char* mac = BLEdata["id"].as(); - Log.trace(F("Processing BLE data %s" CR), BLEdata["id"].as()); - int model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); - int mac_type = BLEdata["mac_type"].as(); - -# if BLEDecryptor - if (BLEdata["encr"] && (BLEdata["encr"].as() >0 && BLEdata["encr"].as() <=2)) { - // Decrypting Encrypted BLE Data PVVX, BTHome or Victron - Log.trace(F("[BLEDecryptor] Decrypt ENCR:%d ModelID:%s Payload:%s" CR), BLEdata["encr"].as(), BLEdata["model_id"].as(), BLEdata["cipher"].as()); - - // MAC address - String macWOdots = BLEdata["id"].as(); // Mac Address without dots - macWOdots.replace(":", ""); - unsigned char macAddress[6]; - int maclen = hexToBytes(macWOdots, macAddress, 6); - if (maclen != 6) { - Log.error(F("[BLEDecryptor] Invalid MAC Address length %d" CR), maclen); - return; - } - - // AES decryption key - unsigned char bleaeskey[16]; - int bleaeskeylength = 0; - if (ble_aes_keys.containsKey(macWOdots)){ - Log.trace(F("[BLEDecryptor] Custom AES key %s" CR), ble_aes_keys[macWOdots].as()); - bleaeskeylength = hexToBytes(ble_aes_keys[macWOdots], bleaeskey, 16); - } else { - Log.trace(F("[BLEDecryptor] Default AES key" CR)); - bleaeskeylength = hexToBytes(ble_aes, bleaeskey, 16); - } - // Check AES Key - if (bleaeskeylength != 16) { - Log.error(F("[BLEDecryptor] Invalid key length %d" CR), bleaeskeylength); - return; - } - - // Build nonce and aad - uint8_t nonce[16]; - int noncelength = 0; - unsigned char aad[1]; - int aadLength; - - if (BLEdata["encr"].as() == 1){ // PVVX Encrypted - noncelength = 11; // 11 bytes - reverseBytes(macAddress, 6); // 6 bytes: device address in reverse - memcpy(nonce, macAddress, 6); - int maclen = hexToBytes(macWOdots, macAddress, 6); - - unsigned char servicedata[16]; - int servicedatalen = hexToBytes(BLEdata["servicedata"].as(), servicedata, 16); - nonce[6] = servicedatalen + 3; // 1 byte : length of (service data + type and UUID) - nonce[7] = 0x16; // 1 byte : "16" -> AD type for "Service Data - 16-bit UUID" - nonce[8] = 0x1A; // 2 bytes: "1a18" -> UUID 181a in little-endian - nonce[9] = 0x18; // - unsigned char ctr[1]; // 1 byte : counter - int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 1); - if (ctrlen != 1) { - Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); - return; - } - nonce[10] = ctr[0]; - aad[0] = 0x11; - aadLength = 1; - Log.trace(F("[BLEDecryptor] PVVX nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); - - } else if (BLEdata["encr"].as() == 2){ // BTHome V2 Encrypted - noncelength = 13; // 13 bytes - memcpy(nonce, macAddress, 6); - nonce[6] = 0xD2; // UUID - nonce[7] = 0xFC; - nonce[8] = 0x41; // BTHome Device Data encrypted payload byte - unsigned char ctr[4]; // Counter - int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 4); - if (ctrlen != 4) { - Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); - return; - } - memcpy(&nonce[9], ctr, 4); - aad[0] = 0x00; - aadLength = 0; - Log.trace(F("[BLEDecryptor] BTHomeV2 nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); - - } else if (BLEdata["encr"].as() == 3){ - nonce[16] = {0}; // Victron has a 16 byte zero padded nonce with IV bytes 6,7 - unsigned char iv[2]; - int ivlen = hexToBytes(BLEdata["ctr"].as(), iv, 2); - if (ivlen != 2) { - Log.error(F("[BLEDecryptor] Invalid iv length %d" CR), ivlen); - return; - } - memcpy(nonce, iv, 2); - Log.trace(F("[BLEDecryptor] Victron nonce %s" CR), NimBLEUtils::dataToHexString(nonce, 16).c_str()); - } else { - return; // No match - } - - // Ciphertext to bytes - int cipherlen = sizeof(BLEdata["cipher"].as()); - unsigned char ciphertext[cipherlen]; - int ciphertextlen = hexToBytes(BLEdata["cipher"].as(), ciphertext, cipherlen); - unsigned char decrypted[ciphertextlen]; // Decrypted payload - - // Decrypt ciphertext - if (BLEdata["encr"].as() == 1 || BLEdata["encr"].as() == 2) { - // Decrypt PVVX and BTHome V2 ciphertext using AES CCM - mbedtls_ccm_context ctx; - mbedtls_ccm_init(&ctx); - if (mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, bleaeskey, 128) != 0) { - Log.error(F("[BLEDecryptor] Failed to set AES key to mbedtls" CR)); - return; - } - - // Message Integrity Check (MIC) - unsigned char mic[4]; - int miclen = hexToBytes(BLEdata["mic"].as(), mic, 4); - if (miclen != 4) { - Log.error(F("[BLEDecryptor] Invalid MIC length %d" CR), miclen); - return; - } - - int ret = mbedtls_ccm_auth_decrypt( - &ctx, // AES Key - ciphertextlen, // length of ciphertext - nonce, noncelength, // Nonce - aad, aadLength, // AAD - ciphertext, // input ciphertext - decrypted, // output plaintext - mic, sizeof(mic) // Message Integrity Check - ); - mbedtls_ccm_free(&ctx); - - if (ret == 0) { - Log.notice(F("[BLEDecryptor] Decryption successful" CR)); - } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { - Log.error(F("[BLEDecryptor] Authentication failed." CR)); - return; - } else { - Log.error(F("[BLEDecryptor] Decryption failed with error: %X" CR), ret); - return; - } - - // Build new servicedata - if (BLEdata["encr"].as() == 1){ // PVVX - BLEdata["servicedata"] = NimBLEUtils::dataToHexString(decrypted, ciphertextlen); - } else if (BLEdata["encr"].as() == 2) { // BTHomeV2 - // Build new servicedata - uint8_t newservicedata[3 + ciphertextlen]; - newservicedata[0] = 0x40; // Decrypted BTHomeV2 Packet Type - newservicedata[1] = 0x00; // Packet counter which the PVVX BTHome non-encrypted has but the encrypted does not - newservicedata[2] = 0x00; // **TODO Convert the ctr to the packet counter or just stick with 0? - memcpy(&newservicedata[3], decrypted, ciphertextlen); - BLEdata["servicedata"] = NimBLEUtils::dataToHexString(newservicedata, ciphertextlen + 3); - } else { - return; - } - Log.trace(F("[BLEDecryptor] Decrypted servicedata %s" CR), BLEdata["servicedata"].as()); - - } else if (BLEdata["encr"].as() == 3) { - // Decrypt Victron Energy encrypted advertisements. - size_t nc_off = 0; - uint8_t stream_block[16] = {0}; - - mbedtls_aes_context ctx; - mbedtls_aes_init(&ctx); - mbedtls_aes_setkey_enc(&ctx, bleaeskey, 128); - int ret = mbedtls_aes_crypt_ctr( - &ctx, // AES Key - ciphertextlen, // length of ciphertext - &nc_off, - nonce, // 16 byte nonce with 2 bytes iv - stream_block, - ciphertext, // input ciphertext - decrypted // output plaintext - ); - mbedtls_aes_free(&ctx); - - if (ret == 0) { - Log.notice(F("[BLEDecryptor] Victron Decryption successful" CR)); - } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { - Log.error(F("[BLEDecryptor] Victron Authentication failed." CR)); - return; - } else { - Log.error(F("[BLEDecryptor] Victron decryption failed with error: %X" CR), ret); - return; - } - - // Build new manufacturerdata - unsigned char manufacturerdata[10 + ciphertextlen]; - int manufacturerdatalen = hexToBytes(BLEdata["manufacturerdata"].as(), manufacturerdata, 10); - manufacturerdata[2] = 0x11; // Replace byte 2 with "11" indicate decrypted data - manufacturerdata[7] = 0xff; // Replace byte 7 with "ff" to indicate decrypted data - manufacturerdata[8] = 0xff; // Replace byte 8 with "ff" to indicate decrypted data - memcpy(&manufacturerdata[8], decrypted, ciphertextlen); // Append the decrypted payload to the manufacturer data - BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString(manufacturerdata, 10 + ciphertextlen); // Rebuild manufacturerdata - Log.trace(F("[BLEDecryptor] Victron decrypted manufacturerdata %s" CR), BLEdata["manufacturerdata"].as()); - } - - // Print before and after decoder post decryption - // serializeJsonPretty(BLEdata, Serial); - model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); - // serializeJsonPretty(BLEdata, Serial); - Log.trace(F("[BLEDecryptor] Decrypted model_id %d" CR), model_id); - - // Remove the cipher fields from BLEdata - BLEdata.remove("encr"); - BLEdata.remove("cipher"); - BLEdata.remove("ctr"); - BLEdata.remove("mic"); - - } -# endif - - // Convert prmacs to RMACS until or if OMG gets Identity MAC/IRK decoding - if (BLEdata["prmac"]) { - BLEdata.remove("prmac"); - if (BLEdata["track"]) { - BLEdata.remove("track"); - } - BLEdata["type"] = "RMAC"; - Log.trace(F("Potential RMAC (prmac) converted to RMAC" CR)); - } - const char* deviceName = BLEdata["name"] | ""; - - 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 - 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 - createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); - } else { - createOrUpdateDevice(mac, device_flags_init, model_id, mac_type, deviceName); - if (BTConfig.adaptiveScan == true && (BTConfig.BLEinterval != MinTimeBtwScan || BTConfig.intervalActiveScan != MinTimeBtwScan)) { - if (BLEdata.containsKey("acts") && BLEdata.containsKey("cont")) { - if (BLEdata["acts"] && BLEdata["cont"]) { - BTConfig.BLEinterval = MinTimeBtwScan; - BTConfig.intervalActiveScan = MinTimeBtwScan; - BTConfig.scanDuration = MinScanDuration; - Log.notice(F("Active and continuous scanning required, parameters adapted" CR)); - // stateBTMeasures(false); - } - } else if (BLEdata.containsKey("cont") && BTConfig.BLEinterval != MinTimeBtwScan) { - if (BLEdata["cont"]) { - BTConfig.BLEinterval = MinTimeBtwScan; - if ((BLEdata["type"].as()).compare("CTMO") == 0) { - BTConfig.scanDuration = MinScanDuration; - } - Log.notice(F("Passive continuous scanning required, parameters adapted" CR)); - // stateBTMeasures(false); - } - } - } - } - } else { - if (BLEdata.containsKey("name")) { // Connectable only devices - std::string name = BLEdata["name"]; - if (name.compare("LYWSD03MMC") == 0) - model_id = BLEconectable::id::LYWSD03MMC; - else if (name.compare("DT24-BLE") == 0) - model_id = BLEconectable::id::DT24_BLE; - else if (name.compare("MHO-C401") == 0) - model_id = BLEconectable::id::MHO_C401; - else if (name.compare("XMWSDJ04MMC") == 0) - model_id = BLEconectable::id::XMWSDJ04MMC; - - if (model_id > 0) { - Log.trace(F("Connectable device found: %s" CR), name.c_str()); - createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); - } - } else if (BTConfig.extDecoderEnable && model_id < 0 && BLEdata.containsKey("servicedata")) { - const char* service_data = (const char*)(BLEdata["servicedata"] | ""); - if (strstr(service_data, "209800") != NULL) { - model_id = TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC; - Log.trace(F("Connectable device found: HHCCJCY01HHCC" CR)); - createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); - } - } - } - } else { - Log.trace(F("Random MAC or iBeacon device filtered" CR)); - } - if (!BTConfig.extDecoderEnable && model_id < 0) { - Log.trace(F("No eligible device found " CR)); - } -} -void PublishDeviceData(JsonObject& BLEdata) { - if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough - // Decode the payload - process_bledata(BLEdata); - // If the device is a random MAC and pubRandomMACs is false we don't publish this payload - if (!BTConfig.pubRandomMACs && (BLEdata["type"].as()).compare("RMAC") == 0) { - Log.trace(F("Random MAC, device filtered" CR)); - return; - } - // If pubAdvData is false we don't publish the adv data - if (!BTConfig.pubAdvData) { - BLEdata.remove("servicedatauuid"); - BLEdata.remove("servicedata"); - BLEdata.remove("manufacturerdata"); - BLEdata.remove("mac_type"); - BLEdata.remove("adv_type"); - // tag device properties - // BLEdata.remove("type"); type is used by the WebUI module to determine the template used to display the signal - BLEdata.remove("cidc"); - BLEdata.remove("acts"); - BLEdata.remove("cont"); - BLEdata.remove("track"); - BLEdata.remove("ctrl"); - } - // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid - if (BLEdata.containsKey("distance")) { - if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { - BLEdata["mac"] = BLEdata["id"].as(); - BLEdata["id"] = BLEdata["uuid"].as(); - } - String topic = String(mqtt_topic) + BTConfig.presenceTopic + String(gateway_name); - Log.trace(F("Pub HA Presence %s" CR), topic.c_str()); - BLEdata["topic"] = topic; - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } - - // If the device is not a sensor and pubOnlySensors is true we don't publish this payload - if (!BTConfig.pubOnlySensors || BLEdata.containsKey("model") || !BLEDecoder) { // Identified device - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } else { - Log.notice(F("Not a sensor device filtered" CR)); - return; - } - -# if BLEDecoder - if (enableMultiGTWSync && BLEdata.containsKey("model_id") && BLEdata.containsKey("id")) { - // Publish tracker sync message - bool isTracker = false; - std::string tag = decoder.getTheengAttribute(BLEdata["model_id"].as(), "tag"); - if (tag.length() >= 4) { - isTracker = checkIfIsTracker(tag[3]); - } - - if (isTracker) { - StaticJsonDocument BLEdataBuffer; - JsonObject TrackerSyncdata = BLEdataBuffer.to(); - TrackerSyncdata["gatewayid"] = gateway_name; - TrackerSyncdata["trackerid"] = BLEdata["id"].as(); - String topic = String(mqtt_topic) + String(subjectTrackerSync); - TrackerSyncdata["topic"] = topic.c_str(); - enqueueJsonObject(TrackerSyncdata); - } - } -# endif - } else { - Log.notice(F("Low rssi, device filtered" CR)); - return; - } -} -# else -void process_bledata(JsonObject& BLEdata) {} -void PublishDeviceData(JsonObject& BLEdata) { - if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough - // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid - if (BLEdata.containsKey("distance")) { - if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { - BLEdata["mac"] = BLEdata["id"].as(); - BLEdata["id"] = BLEdata["uuid"].as(); - } - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } else { - Log.notice(F("Low rssi, device filtered" CR)); - return; - } -} -# endif - -void hass_presence(JsonObject& HomePresence) { - int BLErssi = HomePresence["rssi"]; - Log.trace(F("BLErssi %d" CR), BLErssi); - int txPower = HomePresence["txpower"] | 0; - if (txPower >= 0) - txPower = -59; //if tx power is not found we set a default calibration value - Log.trace(F("TxPower: %d" CR), txPower); - double ratio = BLErssi * 1.0 / txPower; - double distance; - if (ratio < 1.0) { - distance = pow(ratio, 10); - } else { - distance = (0.89976) * pow(ratio, 7.7095) + 0.111; - } - HomePresence["distance"] = distance; - Log.trace(F("Ble distance %D" CR), distance); -} - -void BTforceScan() { - if (!BTProcessLock) { - BLEscan(); - Log.trace(F("Scan done" CR)); - if (BTConfig.bleConnect) - BLEconnect(); - } else { - Log.trace(F("Cannot launch scan due to other process running" CR)); - } -} - -void immediateBTAction(void* pvParameters) { - if (BLEactions.size()) { - // Immediate action; we need to prevent the normal connection action and stop scanning - BTProcessLock = true; - NimBLEScan* pScan = NimBLEDevice::getScan(); - if (pScan->isScanning()) { - pScan->stop(); - } - - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { - // swap the vectors so only this device is processed - std::vector dev_swap; - dev_swap.push_back(getDeviceByMac(BLEactions.back().addr.toString().c_str())); - std::swap(devices, dev_swap); - - std::vector act_swap; - act_swap.push_back(BLEactions.back()); - BLEactions.pop_back(); - std::swap(BLEactions, act_swap); - - // Unlock here to allow the action to be performed - BTProcessLock = false; - BLEconnect(); - // back to normal - std::swap(devices, dev_swap); - std::swap(BLEactions, act_swap); - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - } else { - Log.error(F("CreateOrUpdate Semaphore NOT taken" CR)); - } - - // If we stopped the scheduled connect for this action, do the scheduled now - if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { - timeBetweenConnect = millis(); - BLEconnect(); - } - xSemaphoreGive(semaphoreBLEOperation); - } else { - Log.error(F("BLE busy - immediateBTAction not sent" CR)); - gatewayState = GatewayState::ERROR; - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = BLEactions.back().addr.toString(); - BLEdata["success"] = false; - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - BLEactions.pop_back(); - BTProcessLock = false; - } - } - vTaskDelete(NULL); -} - -void startBTActionTask() { - TaskHandle_t th; - xTaskCreateUniversal( - immediateBTAction, /* Function to implement the task */ - "imActTask", /* Name of the task */ - 8000, /* Stack size in bytes */ - NULL, /* Task input parameter */ - 3, /* Priority of the task (set higher than core task) */ - &th, /* Task handle. */ - 1); /* Core where the task should run */ -} - -# if BLEDecoder -void KnownBTActions(JsonObject& BTdata) { - if (!BTdata.containsKey("id")) { - Log.error(F("BLE mac address missing" CR)); - gatewayState = GatewayState::ERROR; - return; - } - - BLEAction action{}; - action.write = true; - action.ttl = 3; - bool res = false; - if (BTdata.containsKey("model_id") && BTdata["model_id"].is()) { - if (BTdata["model_id"] == "X1") { - if (BTdata.containsKey("cmd") && BTdata["cmd"].is()) { - action.value_type = BLE_VAL_STRING; - std::string val = BTdata["cmd"].as(); // Fix #1694 - action.value = val; - createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, - TheengsDecoder::BLE_ID_NUM::SBS1, 1); - res = true; - } - } else if (BTdata["model_id"] == "W270160X") { - if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { - action.value_type = BLE_VAL_INT; - res = true; - } else if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { - action.value_type = BLE_VAL_STRING; - res = true; - } - if (res) { - std::string val = BTdata["tilt"].as(); // Fix #1694 - action.value = val; - createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, - TheengsDecoder::BLE_ID_NUM::SBBT, 1); - } - } else if (BTdata["model_id"] == "W070160X") { - if (BTdata.containsKey("position") && BTdata["position"].is()) { - action.value_type = BLE_VAL_INT; - res = true; - } else if (BTdata.containsKey("position") && BTdata["position"].is()) { - action.value_type = BLE_VAL_STRING; - res = true; - } - if (res) { - std::string val = BTdata["position"].as(); // Fix #1694 - action.value = val; - createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, - TheengsDecoder::BLE_ID_NUM::SBCU, 1); - } - } - if (res) { - action.addr = NimBLEAddress(BTdata["id"].as(), 1); - BLEactions.push_back(action); - startBTActionTask(); - } else { - Log.error(F("BLE action not recognized" CR)); - gatewayState = GatewayState::ERROR; - } - } -} -# else -void KnownBTActions(JsonObject& BTdata) {} -# endif - -void XtoBTAction(JsonObject& BTdata) { - BLEAction action{}; - action.ttl = BTdata.containsKey("ttl") ? (uint8_t)BTdata["ttl"] : 1; - action.value_type = BLE_VAL_STRING; - if (BTdata.containsKey("value_type")) { - String vt = BTdata["value_type"]; - vt.toUpperCase(); - if (vt == "HEX") - action.value_type = BLE_VAL_HEX; - else if (vt == "INT") - action.value_type = BLE_VAL_INT; - else if (vt == "FLOAT") - action.value_type = BLE_VAL_FLOAT; - else if (vt != "STRING") { - Log.error(F("BLE value type invalid %s" CR), vt.c_str()); - return; - } - } - - Log.trace(F("BLE ACTION TTL = %u" CR), action.ttl); - action.complete = false; - if (BTdata.containsKey("ble_write_address") && - BTdata.containsKey("ble_write_service") && - BTdata.containsKey("ble_write_char") && - BTdata.containsKey("ble_write_value")) { - action.addr = NimBLEAddress(BTdata["ble_write_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); - action.service = NimBLEUUID((const char*)BTdata["ble_write_service"]); - action.characteristic = NimBLEUUID((const char*)BTdata["ble_write_char"]); - std::string val = BTdata["ble_write_value"].as(); // Fix #1694 - action.value = val; - action.write = true; - Log.trace(F("BLE ACTION Write" CR)); - } else if (BTdata.containsKey("ble_read_address") && - BTdata.containsKey("ble_read_service") && - BTdata.containsKey("ble_read_char")) { - action.addr = NimBLEAddress(BTdata["ble_read_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); - action.service = NimBLEUUID((const char*)BTdata["ble_read_service"]); - action.characteristic = NimBLEUUID((const char*)BTdata["ble_read_char"]); - action.write = false; - Log.trace(F("BLE ACTION Read" CR)); - } else { - return; - } - - createOrUpdateDevice(action.addr.toString().c_str(), device_flags_connect, UNKWNON_MODEL, action.addr.getType()); - - BLEactions.push_back(action); - if (BTdata.containsKey("immediate") && BTdata["immediate"].as()) { - startBTActionTask(); - } -} - -void XtoBT(const char* topicOri, JsonObject& BTdata) { // json object decoding - if (cmpToMainTopic(topicOri, subjectMQTTtoBTset)) { - Log.trace(F("MQTTtoBT json set" CR)); - - // Black list & white list set - bool WorBupdated; - WorBupdated = updateWorB(BTdata, true); - WorBupdated |= updateWorB(BTdata, false); - - if (WorBupdated) { - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { - //dumpDevices(); - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - } - } - - // Force scan now - if (BTdata.containsKey("interval") && BTdata["interval"] == 0) { - Log.notice(F("BLE forced scan" CR)); - atomic_store_explicit(&forceBTScan, 1, ::memory_order_seq_cst); // ask the other core to do the scan for us - } - - /* - * Configuration modifications priorities: - * First `init=true` and `load=true` commands are executed (if both are present, INIT prevails on LOAD) - * Then parameters included in json are taken in account - * Finally `erase=true` and `save=true` commands are executed (if both are present, ERASE prevails on SAVE) - */ - if (BTdata.containsKey("init") && BTdata["init"].as()) { - // Restore the default (initial) configuration - BTConfig_init(); - } else if (BTdata.containsKey("load") && BTdata["load"].as()) { - // Load the saved configuration, if not initialised - BTConfig_load(); - } - - // Load config from json if available - BTConfig_fromJson(BTdata); - - } else if (cmpToMainTopic(topicOri, subjectMQTTtoBT)) { - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { - KnownBTActions(BTdata); - XtoBTAction(BTdata); - xSemaphoreGive(semaphoreBLEOperation); - } else { - Log.error(F("BLE busy - BTActions not sent" CR)); - gatewayState = GatewayState::ERROR; - } - } else if (strstr(topicOri, subjectTrackerSync) != NULL) { - if (BTdata.containsKey("gatewayid") && BTdata.containsKey("trackerid") && BTdata["gatewayid"] != gateway_name) { - BLEdevice* device = getDeviceByMac(BTdata["trackerid"].as()); - if (device != &NO_BT_DEVICE_FOUND && device->lastUpdate != 0) { - device->lastUpdate = 0; - Log.notice(F("Tracker %s disassociated by gateway %s" CR), BTdata["trackerid"].as(), BTdata["gatewayid"].as()); - } - } - } -} -#endif +/* + OpenMQTTGateway - ESP8266 or Arduino program for home automation + + Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker + Send and receiving command by MQTT + + This gateway enables to: + - publish MQTT data to a topic related to BLE devices data + + Copyright: (c)Florian ROBERT + + This file is part of OpenMQTTGateway. + + OpenMQTTGateway is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenMQTTGateway is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Thanks to wolass https://github.com/wolass for suggesting me HM 10 and dinosd https://github.com/dinosd/BLE_PROXIMITY for inspiring me how to implement the gateway +*/ +#include "User_config.h" + +#ifdef ZgatewayBT +# include "TheengsCommon.h" + +SemaphoreHandle_t semaphoreCreateOrUpdateDevice; +SemaphoreHandle_t semaphoreBLEOperation; +QueueHandle_t BLEQueue; +unsigned long scanCount = 0; +# include +# include +# include +# include +# include +# include + +# include + +# include "TheengsCommon.h" +# include "config_mqttDiscovery.h" +# include "gatewayBLEConnect.h" +# include "soc/timer_group_reg.h" +# include "soc/timer_group_struct.h" + +using namespace std; + +// Global struct to store live BT configuration data +BTConfig_s BTConfig; + +# if BLEDecoder +# include +# if BLEDecryptor +# include "mbedtls/ccm.h" +# include "mbedtls/aes.h" +# endif +TheengsDecoder decoder; +# endif + +static TaskHandle_t xCoreTaskHandle; +static TaskHandle_t xProcBLETaskHandle; + +struct decompose { + int start; + int len; + bool reverse; +}; + +vector BLEactions; + +vector devices; +int newDevices = 0; + +static BLEdevice NO_BT_DEVICE_FOUND = { + {0}, + 0, + false, + false, + false, + false, + (char)UNKWNON_MODEL, + 0, +}; +static bool oneWhite = false; + +extern bool BTProcessLock; +extern int queueLength; + +void setupBTTasksAndBLE(); +bool checkIfIsTracker(char ch); +void hass_presence(JsonObject& HomePresence); +void BTforceScan(); + +void BTConfig_init() { + BTConfig.bleConnect = AttemptBLEConnect; + BTConfig.BLEinterval = TimeBtwRead; + BTConfig.adaptiveScan = AdaptiveBLEScan; + BTConfig.intervalActiveScan = TimeBtwActive; + BTConfig.intervalConnect = TimeBtwConnect; + BTConfig.scanDuration = Scan_duration; + BTConfig.pubOnlySensors = PublishOnlySensors; + BTConfig.pubRandomMACs = PublishRandomMACs; + BTConfig.presenceEnable = HassPresence; + BTConfig.presenceTopic = subjectHomePresence; + BTConfig.presenceUseBeaconUuid = useBeaconUuidForPresence; + BTConfig.minRssi = MinimumRSSI; + BTConfig.extDecoderEnable = UseExtDecoder; + BTConfig.extDecoderTopic = MQTTDecodeTopic; + BTConfig.filterConnectable = BLE_FILTER_CONNECTABLE; + BTConfig.pubAdvData = pubBLEAdvData; + BTConfig.pubBeaconUuidForTopic = useBeaconUuidForTopic; + BTConfig.ignoreWBlist = false; + BTConfig.presenceAwayTimer = PresenceAwayTimer; + BTConfig.movingTimer = MovingTimer; + BTConfig.forcePassiveScan = false; + BTConfig.enabled = EnableBT; +} + +unsigned long timeBetweenConnect = 0; +unsigned long timeBetweenActive = 0; + +String stateBTMeasures(bool start) { + StaticJsonDocument jsonBuffer; + JsonObject jo = jsonBuffer.to(); + jo["bleconnect"] = BTConfig.bleConnect; + jo["interval"] = BTConfig.BLEinterval; + jo["adaptivescan"] = BTConfig.adaptiveScan; + jo["intervalacts"] = BTConfig.intervalActiveScan; + jo["intervalcnct"] = BTConfig.intervalConnect; + jo["scanduration"] = BTConfig.scanDuration; + jo["hasspresence"] = BTConfig.presenceEnable; + jo["prestopic"] = BTConfig.presenceTopic; + jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; + jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value + jo["extDecoderEnable"] = BTConfig.extDecoderEnable; + jo["extDecoderTopic"] = BTConfig.extDecoderTopic; + jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; + jo["ignoreWBlist"] = BTConfig.ignoreWBlist; + jo["forcepscn"] = BTConfig.forcePassiveScan; + jo["tskstck"] = uxTaskGetStackHighWaterMark(xProcBLETaskHandle); + jo["crstck"] = uxTaskGetStackHighWaterMark(xCoreTaskHandle); + jo["enabled"] = BTConfig.enabled; + jo["scnct"] = scanCount; +# if BLEDecoder + jo["onlysensors"] = BTConfig.pubOnlySensors; + jo["randommacs"] = BTConfig.pubRandomMACs; + jo["filterConnectable"] = BTConfig.filterConnectable; + jo["pubadvdata"] = BTConfig.pubAdvData; + jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; + jo["movingtimer"] = BTConfig.movingTimer; +# endif + + if (start) { + Log.notice(F("BT sys: ")); + serializeJsonPretty(jsonBuffer, Serial); + Serial.println(); + return ""; // Do not try to erase/write/send config at startup + } + String output; + serializeJson(jo, output); + jo["origin"] = subjectBTtoMQTT; + enqueueJsonObject(jo, QueueSemaphoreTimeOutTask); + return (output); +} + +void BTConfig_fromJson(JsonObject& BTdata, bool startup = false) { + // Attempts to connect to eligible devices or not + Config_update(BTdata, "bleconnect", BTConfig.bleConnect); + // Identify AdaptiveScan deactivation to pass to continuous mode or activation to come back to default settings + if (startup == false) { + if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == false && BTConfig.presenceEnable == true) { + BTdata["adaptivescan"] = true; + } else if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == true && BTConfig.presenceEnable == false) { + BTdata["adaptivescan"] = false; + } + + if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == false && BTConfig.adaptiveScan == true) { + BTdata["interval"] = MinTimeBtwScan; + BTdata["intervalacts"] = MinTimeBtwScan; + BTdata["scanduration"] = MinScanDuration; + } else if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == true && BTConfig.adaptiveScan == false) { + BTdata["interval"] = TimeBtwRead; + BTdata["intervalacts"] = TimeBtwActive; + BTdata["scanduration"] = Scan_duration; + } + // Identify if the gateway is enabled or not and stop start accordingly + if (BTdata.containsKey("enabled") && BTdata["enabled"] == false && BTConfig.enabled == true) { + // Stop the gateway but without deinit to enable a future BT restart + stopProcessing(false); + } else if (BTdata.containsKey("enabled") && BTdata["enabled"] == true && BTConfig.enabled == false) { + BTProcessLock = false; + setupBTTasksAndBLE(); + } + } + // Home Assistant presence message + Config_update(BTdata, "hasspresence", BTConfig.presenceEnable); + // Time before before active scan + // Scan interval set - and avoid intervalacts to be lower than interval + if (BTdata.containsKey("interval") && BTdata["interval"] != 0) { + BTConfig.adaptiveScan = false; + Config_update(BTdata, "interval", BTConfig.BLEinterval); + if (BTConfig.intervalActiveScan < BTConfig.BLEinterval) { + Config_update(BTdata, "interval", BTConfig.intervalActiveScan); + } + } + // Define if the scan is adaptive or not - and avoid intervalacts to be lower than interval + if (BTdata.containsKey("intervalacts") && BTdata["intervalacts"] < BTConfig.BLEinterval) { + BTConfig.adaptiveScan = false; + // Config_update(BTdata, "interval", BTConfig.intervalActiveScan); + BTConfig.intervalActiveScan = BTConfig.BLEinterval; + } else { + Config_update(BTdata, "intervalacts", BTConfig.intervalActiveScan); + } + // Adaptive scan set + Config_update(BTdata, "adaptivescan", BTConfig.adaptiveScan); + // Time before a connect set + Config_update(BTdata, "intervalcnct", BTConfig.intervalConnect); + // publish all BLE devices discovered or only the identified sensors (like temperature sensors) + Config_update(BTdata, "scanduration", BTConfig.scanDuration); + // define the duration for a scan; in milliseconds + Config_update(BTdata, "onlysensors", BTConfig.pubOnlySensors); + // publish devices which randomly change their MAC addresses + Config_update(BTdata, "randommacs", BTConfig.pubRandomMACs); + // Home Assistant presence message topic + Config_update(BTdata, "prestopic", BTConfig.presenceTopic); + // Home Assistant presence message use iBeacon UUID + Config_update(BTdata, "presuseuuid", BTConfig.presenceUseBeaconUuid); + // Timer to trigger a device state as offline if not seen + Config_update(BTdata, "presenceawaytimer", BTConfig.presenceAwayTimer); + // Timer to trigger a device state as offline if not seen + Config_update(BTdata, "movingtimer", BTConfig.movingTimer); + // Force passive scan + Config_update(BTdata, "forcepscn", BTConfig.forcePassiveScan); + // MinRSSI set + Config_update(BTdata, "minrssi", BTConfig.minRssi); + // Send undecoded device data + Config_update(BTdata, "extDecoderEnable", BTConfig.extDecoderEnable); + // Topic to send undecoded device data + Config_update(BTdata, "extDecoderTopic", BTConfig.extDecoderTopic); + // Sets whether to filter publishing + Config_update(BTdata, "filterConnectable", BTConfig.filterConnectable); + // Publish advertisement data + Config_update(BTdata, "pubadvdata", BTConfig.pubAdvData); + // Use iBeacon UUID as topic, instead of sender (random) MAC address + Config_update(BTdata, "pubuuid4topic", BTConfig.pubBeaconUuidForTopic); + // Disable Whitelist & Blacklist + Config_update(BTdata, "ignoreWBlist", (BTConfig.ignoreWBlist)); + // Enable or disable the BT gateway + Config_update(BTdata, "enabled", BTConfig.enabled); + + stateBTMeasures(startup); + + if (BTdata.containsKey("erase") && BTdata["erase"].as()) { + // Erase config from NVS (non-volatile storage) + preferences.begin(Gateway_Short_Name, false); + if (preferences.isKey("BTConfig")) { + int result = preferences.remove("BTConfig"); + Log.notice(F("BT config erase result: %d" CR), result); + preferences.end(); + return; // Erase prevails on save, so skipping save + } else { + preferences.end(); + Log.notice(F("BT config not found" CR)); + } + } + + if (BTdata.containsKey("save") && BTdata["save"].as()) { + StaticJsonDocument jsonBuffer; + JsonObject jo = jsonBuffer.to(); + jo["bleconnect"] = BTConfig.bleConnect; + jo["interval"] = BTConfig.BLEinterval; + jo["adaptivescan"] = BTConfig.adaptiveScan; + jo["intervalacts"] = BTConfig.intervalActiveScan; + jo["intervalcnct"] = BTConfig.intervalConnect; + jo["scanduration"] = BTConfig.scanDuration; + jo["onlysensors"] = BTConfig.pubOnlySensors; + jo["randommacs"] = BTConfig.pubRandomMACs; + jo["hasspresence"] = BTConfig.presenceEnable; + jo["prestopic"] = BTConfig.presenceTopic; + jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; + jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value + jo["extDecoderEnable"] = BTConfig.extDecoderEnable; + jo["extDecoderTopic"] = BTConfig.extDecoderTopic; + jo["filterConnectable"] = BTConfig.filterConnectable; + jo["pubadvdata"] = BTConfig.pubAdvData; + jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; + jo["ignoreWBlist"] = BTConfig.ignoreWBlist; + jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; + jo["movingtimer"] = BTConfig.movingTimer; + jo["forcepscn"] = BTConfig.forcePassiveScan; + jo["enabled"] = BTConfig.enabled; + // Save config into NVS (non-volatile storage) + String conf = ""; + serializeJson(jsonBuffer, conf); + preferences.begin(Gateway_Short_Name, false); + int result = preferences.putString("BTConfig", conf); + preferences.end(); + Log.notice(F("BT config save: %s, result: %d" CR), conf.c_str(), result); + } +} + +void BTConfig_load() { + StaticJsonDocument jsonBuffer; + preferences.begin(Gateway_Short_Name, true); + if (preferences.isKey("BTConfig")) { + auto error = deserializeJson(jsonBuffer, preferences.getString("BTConfig", "{}")); + preferences.end(); + Log.notice(F("BT config loaded" CR)); + if (error) { + Log.error(F("BT config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity()); + return; + } + if (jsonBuffer.isNull()) { + Log.warning(F("BT config is null" CR)); + return; + } + JsonObject jo = jsonBuffer.as(); + BTConfig_fromJson(jo, true); // Never send MQTT message with config + Log.notice(F("BT config loaded" CR)); + } else { + preferences.end(); + Log.notice(F("BT config not found" CR)); + } +} + +void PublishDeviceData(JsonObject& BLEdata); + +atomic_int forceBTScan; + +void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type = 0, const char* name = ""); + +BLEdevice* getDeviceByMac(const char* mac); // Declared here to avoid pre-compilation issue (misplaced auto declaration by pio) +BLEdevice* getDeviceByMac(const char* mac) { + Log.trace(F("getDeviceByMac %s" CR), mac); + + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + if ((strcmp((*it)->macAdr, mac) == 0)) { + return *it; + } + } + return &NO_BT_DEVICE_FOUND; +} + +bool updateWorB(JsonObject& BTdata, bool isWhite) { + Log.trace(F("update WorB" CR)); + const char* jsonKey = isWhite ? "white-list" : "black-list"; + + int size = BTdata[jsonKey].size(); + if (size == 0) + return false; + + for (int i = 0; i < size; i++) { + const char* mac = BTdata[jsonKey][i]; + createOrUpdateDevice(mac, (isWhite ? device_flags_isWhiteL : device_flags_isBlackL), + UNKWNON_MODEL); + } + + return true; +} + +void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type, const char* name) { + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(30000)) == pdFALSE) { + Log.error(F("Semaphore NOT taken" CR)); + return; + } + BLEdevice* device = getDeviceByMac(mac); + if (device == &NO_BT_DEVICE_FOUND) { + Log.trace(F("add %s" CR), mac); + //new device + device = new BLEdevice(); + strcpy(device->macAdr, mac); + device->isDisc = flags & device_flags_isDisc; + device->isWhtL = flags & device_flags_isWhiteL; + device->isBlkL = flags & device_flags_isBlackL; + device->connect = flags & device_flags_connect; + device->macType = mac_type; + // Check name length + if (strlen(name) > 20) { + Log.warning(F("Name too long, truncating" CR)); + strncpy(device->name, name, 20); + device->name[19] = '\0'; + } else { + strcpy(device->name, name); + } + device->sensorModel_id = model; + device->lastUpdate = millis(); + devices.push_back(device); + newDevices++; + } else { + Log.trace(F("update %s" CR), mac); + device->lastUpdate = millis(); + device->macType = mac_type; + + if (flags & device_flags_isDisc) { + device->isDisc = true; + } + + if (flags & device_flags_connect) { + device->connect = true; + } + + if (model != UNKWNON_MODEL && device->sensorModel_id == UNKWNON_MODEL) { + newDevices++; + device->isDisc = false; + device->sensorModel_id = model; + } + + // If a device has been added to the white-list, flag it so it can be auto-detected + if (!device->isWhtL && flags & device_flags_isWhiteL) { + newDevices++; + } + if (flags & device_flags_isWhiteL || flags & device_flags_isBlackL) { + device->isWhtL = flags & device_flags_isWhiteL; + device->isBlkL = flags & device_flags_isBlackL; + } + } + + // update oneWhite flag + oneWhite = oneWhite || device->isWhtL; + + xSemaphoreGive(semaphoreCreateOrUpdateDevice); +} + +void updateDevicesStatus() { + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + BLEdevice* p = *it; + unsigned long now = millis(); + // Check for tracker status + bool isTracker = false; +# if BLEDecoder + std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); + if (tag.length() >= 4) { + isTracker = checkIfIsTracker(tag[3]); + } + // Device tracker devices + if (isTracker) { // We apply the offline status only for tracking device, can be extended further to all the devices + if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.presenceAwayTimer) && (now > BTConfig.presenceAwayTimer)) && + (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = p->macAdr; + BLEdata["state"] = "offline"; + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + // We set the lastUpdate to 0 to avoid replublishing the offline state + p->lastUpdate = 0; + } + } + // Moving detection devices (devices with an accelerometer) + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BC08) { + if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.movingTimer) && (now > BTConfig.movingTimer)) && + (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = p->macAdr; + BLEdata["state"] = "offline"; + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + // We set the lastUpdate to 0 to avoid replublishing the offline state + p->lastUpdate = 0; + } + } +# endif + } +} + +void dumpDevices() { +# if LOG_LEVEL > LOG_LEVEL_NOTICE + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + BLEdevice* p = *it; + Log.trace(F("macAdr %s" CR), p->macAdr); + Log.trace(F("macType %d" CR), p->macType); + Log.trace(F("isDisc %d" CR), p->isDisc); + Log.trace(F("isWhtL %d" CR), p->isWhtL); + Log.trace(F("isBlkL %d" CR), p->isBlkL); + Log.trace(F("connect %d" CR), p->connect); + Log.trace(F("sensorModel_id %d" CR), p->sensorModel_id); + Log.trace(F("LastUpdate %u" CR), p->lastUpdate); + } +# endif +} + +void strupp(char* beg) { + while ((*beg = toupper(*beg))) + ++beg; +} + +# ifdef ZmqttDiscovery +void DT24Discovery(const char* mac, const char* sensorModel_id) { +# define DT24parametersCount 7 + Log.trace(F("DT24Discovery" CR)); + const char* DT24sensor[DT24parametersCount][9] = { + {"sensor", "volt", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "amp", mac, "current", jsonCurrent, "", "", "A", stateClassMeasurement}, + {"sensor", "watt", mac, "power", jsonPower, "", "", "W", stateClassMeasurement}, + {"sensor", "watt-hour", mac, "power", jsonEnergy, "", "", "kWh", stateClassMeasurement}, + {"sensor", "price", mac, "", jsonMsg, "", "", "", stateClassNone}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"binary_sensor", "inUse", mac, "power", jsonInuse, "", "", "", stateClassNone} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, DT24sensor, DT24parametersCount, "DT24", "ATorch", sensorModel_id); +} + +void BM2Discovery(const char* mac, const char* sensorModel_id) { +# define BM2parametersCount 2 + Log.trace(F("BM2Discovery" CR)); + const char* BM2sensor[BM2parametersCount][9] = { + {"sensor", "volt", mac, "voltage", jsonVoltBM2, "", "", "V", stateClassMeasurement}, // We use a json definition that retrieve only data from the BM2 decoder, as this sensor also advertize volt as an iBeacon + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, BM2sensor, BM2parametersCount, "BM2", "Generic", sensorModel_id); +} + +void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) { +# define LYWSD03MMCparametersCount 4 + Log.trace(F("LYWSD03MMCDiscovery" CR)); + const char* LYWSD03MMCsensor[LYWSD03MMCparametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, LYWSD03MMCsensor, LYWSD03MMCparametersCount, "LYWSD03MMC", "Xiaomi", sensorModel); +} + +void MHO_C401Discovery(const char* mac, const char* sensorModel) { +# define MHO_C401parametersCount 4 + Log.trace(F("MHO_C401Discovery" CR)); + const char* MHO_C401sensor[MHO_C401parametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, MHO_C401sensor, MHO_C401parametersCount, "MHO_C401", "Xiaomi", sensorModel); +} + +void HHCCJCY01HHCCDiscovery(const char* mac, const char* sensorModel) { +# define HHCCJCY01HHCCparametersCount 5 + Log.trace(F("HHCCJCY01HHCCDiscovery" CR)); + const char* HHCCJCY01HHCCsensor[HHCCJCY01HHCCparametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "lux", mac, "illuminance", jsonLux, "", "", "lx", stateClassMeasurement}, + {"sensor", "fer", mac, "", jsonFer, "", "", "µS/cm", stateClassMeasurement}, + {"sensor", "moi", mac, "", jsonMoi, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, HHCCJCY01HHCCsensor, HHCCJCY01HHCCparametersCount, "HHCCJCY01HHCC", "Xiaomi", sensorModel); +} + +void XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel) { +# define XMWSDJ04MMCparametersCount 4 + Log.trace(F("XMWSDJ04MMCDiscovery" CR)); + const char* XMWSDJ04MMCsensor[XMWSDJ04MMCparametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, XMWSDJ04MMCsensor, XMWSDJ04MMCparametersCount, "XMWSDJ04MMC", "Xiaomi", sensorModel); +} + +void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) { + Log.trace(F("xxWSD0xMMCDiscovery" CR)); + int xxWSD0xMMCparametersCount = 5; + if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) xxWSD0xMMCparametersCount = 7; + const char* xxWSD0xMMCsensor[xxWSD0xMMCparametersCount][9] = { + {"sensor", "Battery", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "Voltage", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "Temperature", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "Humidity", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement}, + {"sensor", "RSSI", mac, "signal_strength", jsonRSSI, "", "", "dB", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement, state class + }; + if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) { + const char* power[9] = {"sensor", "Power", mac, "", jsonPower, "", "", "", stateClassNone}; + memcpy(xxWSD0xMMCsensor[4], power, 9); + const char* open[9] = {"sensor", "Opening", mac, "", jsonOpen, "", "", "", stateClassNone}; + memcpy(xxWSD0xMMCsensor[5], open, 9); + } + createDiscoveryFromList(mac, xxWSD0xMMCsensor, xxWSD0xMMCparametersCount, name, "Xiaomi", sensorModel); +} + +# else +void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) {} +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 XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel_id) {} +void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) {} +# endif + +/* + Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleScan.cpp + Ported to Arduino ESP32 by Evandro Copercini + */ +// core task implementation thanks to https://techtutorialsx.com/2017/05/09/esp32-running-code-on-a-specific-core/ + +//core on which the BLE detection task will run +static int taskCore = 0; + +class ScanCallbacks : public NimBLEScanCallbacks { + void onResult(const NimBLEAdvertisedDevice* advertisedDevice) { + NimBLEAdvertisedDevice* ad = new NimBLEAdvertisedDevice(*advertisedDevice); + if (xQueueSend(BLEQueue, &ad, 0) != pdTRUE) { + Log.error(F("BLEQueue full" CR)); + delete (ad); + } + } +} scanCallbacks; + +std::string convertServiceData(std::string deviceServiceData) { + int serviceDataLength = (int)deviceServiceData.length(); + char spr[2 * serviceDataLength + 1]; + for (int i = 0; i < serviceDataLength; i++) sprintf(spr + 2 * i, "%.2x", (unsigned char)deviceServiceData[i]); + spr[2 * serviceDataLength] = 0; + Log.trace(F("Converted service data (%d) to %s" CR), serviceDataLength, spr); + return spr; +} + +bool checkIfIsTracker(char ch) { + uint8_t data = 0; + if (ch >= '0' && ch <= '9') + data = ch - '0'; + else if (ch >= 'a' && ch <= 'f') + data = 10 + (ch - 'a'); + + if (((data >> 3) & 0x01) == 1) { + Log.trace(F("Is Device Tracker" CR)); + return true; + } else { + return false; + } +} + +void procBLETask(void* pvParameters) { + BLEAdvertisedDevice* advertisedDevice = nullptr; + + for (;;) { + xQueueReceive(BLEQueue, &advertisedDevice, portMAX_DELAY); + // Feed the watchdog + //esp_task_wdt_reset(); + if (!BTProcessLock) { + Log.trace(F("Creating BLE buffer" CR)); + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = advertisedDevice->getAddress().toString(); + BLEdata["mac_type"] = advertisedDevice->getAddress().getType(); + BLEdata["adv_type"] = advertisedDevice->getAdvType(); + Log.notice(F("BT Device detected: %s" CR), BLEdata["id"].as()); + BLEdevice* device = getDeviceByMac(BLEdata["id"].as()); + + if (BTConfig.filterConnectable && device->connect) { + Log.notice(F("Filtered connectable device" CR)); + delete (advertisedDevice); + continue; + } + + if (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(device)) && !isBlack(device))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC) + if (advertisedDevice->haveName()) + BLEdata["name"] = (char*)advertisedDevice->getName().c_str(); + if (advertisedDevice->haveManufacturerData()) { + BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString((uint8_t*)advertisedDevice->getManufacturerData().data(), + advertisedDevice->getManufacturerData().length()); + } + BLEdata["rssi"] = (int)advertisedDevice->getRSSI(); + if (advertisedDevice->haveTXPower()) + BLEdata["txpower"] = (int8_t)advertisedDevice->getTXPower(); + if (BTConfig.presenceEnable) { + hass_presence(BLEdata); // with either only sensors or not we can use it for home assistant room presence component + } + if (advertisedDevice->haveServiceData()) { + int serviceDataCount = advertisedDevice->getServiceDataCount(); + Log.trace(F("Get services data number: %d" CR), serviceDataCount); + for (int j = 0; j < serviceDataCount; j++) { + StaticJsonDocument BLEdataBufferTemp; + JsonObject BLEdataTemp = BLEdataBufferTemp.to(); + BLEdataBufferTemp = BLEdataBuffer; + std::string service_data = convertServiceData(advertisedDevice->getServiceData(j)); + Log.trace(F("Service data: %s" CR), service_data.c_str()); + std::string serviceDatauuid = advertisedDevice->getServiceDataUUID(j).toString(); + Log.trace(F("Service data UUID: %s" CR), (char*)serviceDatauuid.c_str()); + BLEdataTemp["servicedata"] = (char*)service_data.c_str(); + BLEdataTemp["servicedatauuid"] = (char*)serviceDatauuid.c_str(); + PublishDeviceData(BLEdataTemp); + } + } else { + PublishDeviceData(BLEdata); + } + } else { + Log.trace(F("Filtered MAC device" CR)); + } + updateDevicesStatus(); + } + delete (advertisedDevice); + vTaskDelay(10); + } +} + +/** + * BLEscan used to retrieve BLE advertized data from devices without connection + */ +void BLEscan() { + // Don't start the next scan until processing of previous results is complete. + while (uxQueueMessagesWaiting(BLEQueue) || queueLength != 0) { // the criteria on queueLength could be adjusted to parallelize the scan and the queue processing + delay(1); // Wait for queue to empty, a yield here instead of the delay cause the WDT to trigger + } + Log.notice(F("Scan begin" CR)); + BLEScan* pBLEScan = BLEDevice::getScan(); + pBLEScan->setScanCallbacks(&scanCallbacks); + if ((millis() > (timeBetweenActive + BTConfig.intervalActiveScan) || BTConfig.intervalActiveScan == BTConfig.BLEinterval) && !BTConfig.forcePassiveScan) { + pBLEScan->setActiveScan(true); + timeBetweenActive = millis(); + } else { + pBLEScan->setActiveScan(false); + } + pBLEScan->setInterval(BLEScanInterval); + pBLEScan->setWindow(BLEScanWindow); + NimBLEScanResults foundDevices = pBLEScan->getResults(BTConfig.scanDuration, false); + if (foundDevices.getCount()) + scanCount++; + Log.notice(F("Found %d devices, scan number %d end" CR), foundDevices.getCount(), scanCount); + Log.trace(F("Process BLE stack free: %u" CR), uxTaskGetStackHighWaterMark(xProcBLETaskHandle)); +} + +/** + * Connect to BLE devices and initiate the callbacks with a service/characteristic request + */ +# if BLEDecoder +void BLEconnect() { + if (!BTProcessLock) { + Log.notice(F("BLE Connect begin" CR)); + do { + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + BLEdevice* p = *it; + if (p->connect) { + Log.trace(F("Model to connect found: %s" CR), p->macAdr); + NimBLEAddress addr((const char*)p->macAdr, p->macType); + if (p->sensorModel_id == BLEconectable::id::LYWSD03MMC || + p->sensorModel_id == BLEconectable::id::MHO_C401) { + LYWSD03MMC_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + BLEclient.publishData(); + } else if (p->sensorModel_id == BLEconectable::id::DT24_BLE) { + DT24_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + BLEclient.publishData(); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { + BM2_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); + BLEclient.publishData(); + } else if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { + XMWSDJ04MMC_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + BLEclient.publishData(); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1) { + SBS1_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT) { + SBBT_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU) { + SBCU_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + } else { + GENERIC_connect BLEclient(addr); + if (BLEclient.processActions(BLEactions)) { + // If we don't regularly connect to this, disable connections so advertisements + // won't be filtered if BLE_FILTER_CONNECTABLE is set. + p->connect = false; + } + } + if (BLEactions.size() > 0) { + std::vector swap; + for (auto& it : BLEactions) { + if (!it.complete && --it.ttl) { + swap.push_back(it); + } else if (it.addr == NimBLEAddress(p->macAdr, p->macType)) { + if (p->sensorModel_id != BLEconectable::id::DT24_BLE && + 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 != BLEconectable::id::MHO_C401 && + p->sensorModel_id != BLEconectable::id::XMWSDJ04MMC) { + // if irregulary connected to and connection failed clear the connect flag. + p->connect = false; + } + } + } + std::swap(BLEactions, swap); + } + } + } + } while (BLEactions.size() > 0); + Log.notice(F("BLE Connect end" CR)); + } +} +# else +void BLEconnect() {} +# endif + +void stopProcessing(bool deinit) { + if (BTConfig.enabled) { + BTProcessLock = true; + // We stop the scan + Log.notice(F("Stopping BLE scan" CR)); + BLEScan* pBLEScan = BLEDevice::getScan(); + if (pBLEScan->isScanning()) { + pBLEScan->stop(); + } + + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { + Log.notice(F("Stopping BLE tasks" CR)); + //Suspending, deleting tasks and stopping BT to free memory + vTaskSuspend(xCoreTaskHandle); + vTaskDelete(xCoreTaskHandle); + vTaskSuspend(xProcBLETaskHandle); + vTaskDelete(xProcBLETaskHandle); + xSemaphoreGive(semaphoreBLEOperation); + } + // Using deinit to free memory, should only be used if we are going to restart the gateway + if (deinit) + BLEDevice::deinit(true); + } + Log.notice(F("BLE gateway stopped, free heap: %d" CR), ESP.getFreeHeap()); +} + +void coreTask(void* pvParameters) { + while (true) { + if (!BTProcessLock) { + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(30000)) == pdTRUE) { + BLEscan(); + // Launching a connect every TimeBtwConnect + if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { + timeBetweenConnect = millis(); + BLEconnect(); + } + //dumpDevices(); + Log.trace(F("CoreTask stack free: %u" CR), uxTaskGetStackHighWaterMark(xCoreTaskHandle)); + xSemaphoreGive(semaphoreBLEOperation); + } else { + Log.error(F("Failed to start scan - BLE busy" CR)); + } + if (SYSConfig.powerMode > 0) { + int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); // is this enough, it will wait the full deepsleep... + if (scan == 1) BTforceScan(); + ready_to_sleep = true; + } else { + for (int interval = BTConfig.BLEinterval, waitms; interval > 0; interval -= waitms) { + int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); + if (scan == 1) BTforceScan(); // should we break after this? + delay(waitms = interval > 100 ? 100 : interval); // 100ms + } + } + } + delay(1); + } +} + +void setupBTTasksAndBLE() { +# ifdef CONFIG_BTDM_BLE_SCAN_DUPL + BLEDevice::setScanDuplicateCacheSize(BLEScanDuplicateCacheSize); +# endif + BLEDevice::init(""); + xTaskCreateUniversal( + procBLETask, /* Function to implement the task */ + "procBLETask", /* Name of the task */ +# if defined(USE_ESP_IDF) || defined(USE_BLUFI) + 14500, +# else + 9500, /* Stack size in bytes */ +# endif + NULL, /* Task input parameter */ + 2, /* Priority of the task (set higher than core task) */ + &xProcBLETaskHandle, /* Task handle. */ + 1); /* Core where the task should run */ + + // we setup a task with priority one to avoid conflict with other gateways + xTaskCreateUniversal( + coreTask, /* Function to implement the task */ + "coreTask", /* Name of the task */ + 5120, /* Stack size in bytes */ + NULL, /* Task input parameter */ + 1, /* Priority of the task */ + &xCoreTaskHandle, /* Task handle. */ + taskCore); /* Core where the task should run */ +} + +void setupBT() { + BTConfig_init(); + BTConfig_load(); + Log.notice(F("BLE scans interval: %d" CR), BTConfig.BLEinterval); + Log.notice(F("BLE connects interval: %d" CR), BTConfig.intervalConnect); + Log.notice(F("BLE scan duration: %d" CR), BTConfig.scanDuration); + Log.notice(F("Publishing only BLE sensors: %T" CR), BTConfig.pubOnlySensors); + Log.notice(F("Publishing random MAC devices: %T" CR), BTConfig.pubRandomMACs); + Log.notice(F("Adaptive BLE scan: %T" CR), BTConfig.adaptiveScan); + Log.notice(F("Active BLE scan interval: %d" CR), BTConfig.intervalActiveScan); + Log.notice(F("minrssi: %d" CR), -abs(BTConfig.minRssi)); + Log.notice(F("Presence Away Timer: %d" CR), BTConfig.presenceAwayTimer); + Log.notice(F("Moving Timer: %d" CR), BTConfig.movingTimer); + Log.notice(F("Force passive scan: %T" CR), BTConfig.forcePassiveScan); + Log.notice(F("Enabled BLE: %T" CR), BTConfig.enabled); + + atomic_init(&forceBTScan, 0); // in theory, we don't need this + + semaphoreCreateOrUpdateDevice = xSemaphoreCreateBinary(); + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + + semaphoreBLEOperation = xSemaphoreCreateBinary(); + xSemaphoreGive(semaphoreBLEOperation); + + BLEQueue = xQueueCreate(QueueSize, sizeof(NimBLEAdvertisedDevice*)); + if (BTConfig.enabled) { + setupBTTasksAndBLE(); + Log.notice(F("gatewayBT multicore ESP32 setup done" CR)); + } else { + Log.notice(F("gatewayBT multicore ESP32 setup disabled" CR)); + } +} + +boolean valid_service_data(const char* data, int size) { + for (int i = 0; i < size; ++i) { + if (data[i] != 48) // 48 correspond to 0 in ASCII table + return true; + } + return false; +} + +# if defined(ZmqttDiscovery) && BLEDecoder == true +// This function always should be called from the main core as it generates direct mqtt messages +// When overrideDiscovery=true, we publish discovery messages of known devices (even if no new) +void launchBTDiscovery(bool overrideDiscovery) { + if (!overrideDiscovery && newDevices == 0) + return; + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdFALSE) { + Log.error(F("Semaphore NOT taken" CR)); + return; + } + newDevices = 0; + vector localDevices = devices; + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + for (vector::iterator it = localDevices.begin(); it != localDevices.end(); ++it) { + BLEdevice* p = *it; + Log.trace(F("Device mac %s" CR), p->macAdr); + // Do not launch discovery for the devices already discovered (unless we have overrideDiscovery) or that are not unique by their MAC Address (iBeacon, GAEN and Microsoft CDP) + if (overrideDiscovery || !isDiscovered(p)) { + String macWOdots = String(p->macAdr); + macWOdots.replace(":", ""); + if (p->sensorModel_id >= 0) { + Log.trace(F("Looking for Model_id: %d" CR), p->sensorModel_id); + std::string properties = decoder.getTheengProperties(p->sensorModel_id); + Log.trace(F("properties: %s" CR), properties.c_str()); + std::string brand = decoder.getTheengAttribute(p->sensorModel_id, "brand"); + std::string model = decoder.getTheengAttribute(p->sensorModel_id, "model"); + if (displayDeviceName || ForceDeviceName) { + if (p->name[0] != '\0') { + model = p->name; + } + } + std::string model_id = decoder.getTheengAttribute(p->sensorModel_id, "model_id"); + + // Check for tracker status + bool isTracker = false; + std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); + if (tag.length() >= 4) { + isTracker = checkIfIsTracker(tag[3]); + } + + String discovery_topic = String(subjectBTtoMQTT) + "/" + macWOdots; + 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 + if (isTracker) { + String tracker_name = String(model_id.c_str()) + "-tracker"; + String tracker_id = macWOdots + "-tracker"; + createDiscovery("device_tracker", + discovery_topic.c_str(), tracker_name.c_str(), 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 == TheengsDecoder::BLE_ID_NUM::BC08) { + String sensor_name = String(model_id.c_str()) + "-moving"; + String sensor_id = macWOdots + "-moving"; + createDiscovery("binary_sensor", + discovery_topic.c_str(), sensor_name.c_str(), sensor_id.c_str(), + will_Topic, "moving", "{% if value_json.get('accx') -%}on{%- else -%}off{%- endif %}", + "on", "off", "", + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } + if (p->sensorModel_id >= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_ATC && p->sensorModel_id <= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_PVVX_BTHOME_2 ) { + xxWSD0xMMCDiscovery(macWOdots.c_str(), p->name, model_id.c_str()); + } else if (!properties.empty()) { + StaticJsonDocument jsonBuffer; + auto error = deserializeJson(jsonBuffer, properties); + if (error) { + if (jsonBuffer.overflowed()) { + // This should not happen if JSON_MSG_BUFFER is large enough for + // the Theengs json properties + Log.error(F("JSON deserialization of Theengs properties overflowed (error %s), buffer capacity: %u. Program might crash. Properties json: %s" CR), + error.c_str(), jsonBuffer.capacity(), properties.c_str()); + } else { + Log.error(F("JSON deserialization of Theengs properties errored: %" CR), + error.c_str()); + } + } + for (JsonPair prop : jsonBuffer["properties"].as()) { + Log.trace(F("Key: %s"), prop.key().c_str()); + Log.trace(F("Unit: %s"), prop.value()["unit"].as()); + Log.trace(F("Name: %s"), prop.value()["name"].as()); + String entity_name = ""; + if (displayDeviceName || ForceDeviceName) { + entity_name = String(model.c_str()) + "-" + String(prop.key().c_str()); + } else { + entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); + } + String entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); + String unique_id = macWOdots + "-" + String(prop.key().c_str()); + String value_template = "{{ value_json." + String(prop.key().c_str()) + " | is_defined }}"; + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1 && strcmp(prop.key().c_str(), "state") == 0) { + String payload_on = "{\"model_id\":\"X1\",\"cmd\":\"on\",\"id\":\"" + String(p->macAdr) + "\"}"; + String payload_off = "{\"model_id\":\"X1\",\"cmd\":\"off\",\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("switch", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "switch", value_template.c_str(), + payload_on.c_str(), payload_off.c_str(), "", 0, + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone, "off", "on"); + unique_id = macWOdots + "-press"; + entity_name = String(model_id.c_str()) + "-press"; + String payload_press = "{\"model_id\":\"X1\",\"cmd\":\"press\",\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("button", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "button", "", + payload_press.c_str(), "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT && strcmp(prop.key().c_str(), "open") == 0) { + value_template = "{% if value_json.direction == \"up\" -%} {{ 100 - value_json.open/2 }}{% elif value_json.direction == \"down\" %}{{ value_json.open/2 }}{% else %} {{ value_json.open/2 }}{%- endif %}"; + String command_template = "{\"model_id\":\"W270160X\",\"tilt\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("cover", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "cover", value_template.c_str(), + "50", "", "", 0, + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + "blind", nullptr, nullptr, nullptr, command_template.c_str()); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU && strcmp(prop.key().c_str(), "position") == 0) { + String command_template = "{\"model_id\":\"W070160X\",\"position\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("cover", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "cover", "{{ value_json.position }}", + "0", "100", "", 0, + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + "curtain", nullptr, nullptr, nullptr, command_template.c_str()); + } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && + strcmp(prop.key().c_str(), "weighing_mode") == 0) { + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "enum", value_template.c_str(), + "", "", prop.value()["unit"], + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassMeasurement, nullptr, nullptr, "[\"person\",\"object\"]"); + } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && + strcmp(prop.key().c_str(), "unit") == 0) { + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "enum", value_template.c_str(), + "", "", prop.value()["unit"], + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassMeasurement, nullptr, nullptr, "[\"lb\",\"kg\",\"jin\"]"); + } else if (strcmp(prop.value()["unit"], "string") == 0 && strcmp(prop.key().c_str(), "mac") != 0) { + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "", "", "", + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (p->sensorModel_id == TheengsDecoder::MUE4094RT && strcmp(prop.value()["unit"], "status") == 0) { // This device does not a broadcast when there is nothing detected so adding a timeout + createDiscovery("binary_sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "True", "False", "", + BTConfig.presenceAwayTimer / 1000, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (strcmp(prop.value()["unit"], "status") == 0) { + createDiscovery("binary_sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "True", "False", "", + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (strcmp(prop.key().c_str(), "device") != 0 && strcmp(prop.key().c_str(), "mac") != 0) { // Exception on device and mac as these ones are not sensors + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "", "", prop.value()["unit"], + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassMeasurement); + } + } + } + } 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) { + // 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"); + } + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { + // Sensor discovery + BM2Discovery(macWOdots.c_str(), "BM2"); + // Device tracker discovery + String tracker_id = macWOdots + "-tracker"; + createDiscovery("device_tracker", + discovery_topic.c_str(), "BM2-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"); + } + if (p->sensorModel_id == BLEconectable::id::MHO_C401) { + MHO_C401Discovery(macWOdots.c_str(), "MHO-C401"); + } + if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { + XMWSDJ04MMCDiscovery(macWOdots.c_str(), "XMWSDJ04MMC"); + } + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC) { + HHCCJCY01HHCCDiscovery(macWOdots.c_str(), "HHCCJCY01HHCC"); + } + } else { + Log.trace(F("Device UNKNOWN_MODEL %s" CR), p->macAdr); + } + } + } + p->isDisc = true; // we don't need the semaphore and all the search magic via createOrUpdateDevice + } else { + Log.trace(F("Device already discovered or that doesn't require discovery %s" CR), p->macAdr); + } + } +} +# else +void launchBTDiscovery(bool overrideDiscovery) {} +# endif + +# if BLEDecryptor +// ** TODO - Hex string to bytes, there is probably a function for this already just need to find it +int hexToBytes(String hex, uint8_t *out, size_t maxLen) { + int len = hex.length(); + if (len % 2 || len / 2 > maxLen) return -1; + for (int i = 0, j = 0; i < len; i += 2, j++) { + out[j] = (uint8_t) strtol(hex.substring(i, i + 2).c_str(), nullptr, 16); + } + return len / 2; +} +// Reverse bytes +void reverseBytes(uint8_t *data, size_t length) { + size_t i; + for (i = 0; i < length / 2; i++) { + uint8_t temp = data[i]; + data[i] = data[length - 1 - i]; + data[length - 1 - i] = temp; + } +} +# endif + +# if BLEDecoder +void process_bledata(JsonObject& BLEdata) { + yield(); // Necessary to let the loop run in case of connectivity issues + if (!BLEdata.containsKey("id")) { + Log.error(F("No mac address in the payload" CR)); + return; + } + const char* mac = BLEdata["id"].as(); + Log.trace(F("Processing BLE data %s" CR), BLEdata["id"].as()); + int model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); + int mac_type = BLEdata["mac_type"].as(); + +# if BLEDecryptor + if (BLEdata["encr"] && (BLEdata["encr"].as() >0 && BLEdata["encr"].as() <=2)) { + // Decrypting Encrypted BLE Data PVVX, BTHome or Victron + Log.trace(F("[BLEDecryptor] Decrypt ENCR:%d ModelID:%s Payload:%s" CR), BLEdata["encr"].as(), BLEdata["model_id"].as(), BLEdata["cipher"].as()); + + // MAC address + String macWOdots = BLEdata["id"].as(); // Mac Address without dots + macWOdots.replace(":", ""); + unsigned char macAddress[6]; + int maclen = hexToBytes(macWOdots, macAddress, 6); + if (maclen != 6) { + Log.error(F("[BLEDecryptor] Invalid MAC Address length %d" CR), maclen); + return; + } + + // AES decryption key + unsigned char bleaeskey[16]; + int bleaeskeylength = 0; + if (ble_aes_keys.containsKey(macWOdots)){ + Log.trace(F("[BLEDecryptor] Custom AES key %s" CR), ble_aes_keys[macWOdots].as()); + bleaeskeylength = hexToBytes(ble_aes_keys[macWOdots], bleaeskey, 16); + } else { + Log.trace(F("[BLEDecryptor] Default AES key" CR)); + bleaeskeylength = hexToBytes(ble_aes, bleaeskey, 16); + } + // Check AES Key + if (bleaeskeylength != 16) { + Log.error(F("[BLEDecryptor] Invalid key length %d" CR), bleaeskeylength); + return; + } + + // Build nonce and aad + uint8_t nonce[16]; + int noncelength = 0; + unsigned char aad[1]; + int aadLength; + + if (BLEdata["encr"].as() == 1){ // PVVX Encrypted + noncelength = 11; // 11 bytes + reverseBytes(macAddress, 6); // 6 bytes: device address in reverse + memcpy(nonce, macAddress, 6); + int maclen = hexToBytes(macWOdots, macAddress, 6); + + unsigned char servicedata[16]; + int servicedatalen = hexToBytes(BLEdata["servicedata"].as(), servicedata, 16); + nonce[6] = servicedatalen + 3; // 1 byte : length of (service data + type and UUID) + nonce[7] = 0x16; // 1 byte : "16" -> AD type for "Service Data - 16-bit UUID" + nonce[8] = 0x1A; // 2 bytes: "1a18" -> UUID 181a in little-endian + nonce[9] = 0x18; // + unsigned char ctr[1]; // 1 byte : counter + int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 1); + if (ctrlen != 1) { + Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); + return; + } + nonce[10] = ctr[0]; + aad[0] = 0x11; + aadLength = 1; + Log.trace(F("[BLEDecryptor] PVVX nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); + + } else if (BLEdata["encr"].as() == 2){ // BTHome V2 Encrypted + noncelength = 13; // 13 bytes + memcpy(nonce, macAddress, 6); + nonce[6] = 0xD2; // UUID + nonce[7] = 0xFC; + nonce[8] = 0x41; // BTHome Device Data encrypted payload byte + unsigned char ctr[4]; // Counter + int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 4); + if (ctrlen != 4) { + Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); + return; + } + memcpy(&nonce[9], ctr, 4); + aad[0] = 0x00; + aadLength = 0; + Log.trace(F("[BLEDecryptor] BTHomeV2 nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); + + } else if (BLEdata["encr"].as() == 3){ + nonce[16] = {0}; // Victron has a 16 byte zero padded nonce with IV bytes 6,7 + unsigned char iv[2]; + int ivlen = hexToBytes(BLEdata["ctr"].as(), iv, 2); + if (ivlen != 2) { + Log.error(F("[BLEDecryptor] Invalid iv length %d" CR), ivlen); + return; + } + memcpy(nonce, iv, 2); + Log.trace(F("[BLEDecryptor] Victron nonce %s" CR), NimBLEUtils::dataToHexString(nonce, 16).c_str()); + } else { + return; // No match + } + + // Ciphertext to bytes + int cipherlen = sizeof(BLEdata["cipher"].as()); + unsigned char ciphertext[cipherlen]; + int ciphertextlen = hexToBytes(BLEdata["cipher"].as(), ciphertext, cipherlen); + unsigned char decrypted[ciphertextlen]; // Decrypted payload + + // Decrypt ciphertext + if (BLEdata["encr"].as() == 1 || BLEdata["encr"].as() == 2) { + // Decrypt PVVX and BTHome V2 ciphertext using AES CCM + mbedtls_ccm_context ctx; + mbedtls_ccm_init(&ctx); + if (mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, bleaeskey, 128) != 0) { + Log.error(F("[BLEDecryptor] Failed to set AES key to mbedtls" CR)); + return; + } + + // Message Integrity Check (MIC) + unsigned char mic[4]; + int miclen = hexToBytes(BLEdata["mic"].as(), mic, 4); + if (miclen != 4) { + Log.error(F("[BLEDecryptor] Invalid MIC length %d" CR), miclen); + return; + } + + int ret = mbedtls_ccm_auth_decrypt( + &ctx, // AES Key + ciphertextlen, // length of ciphertext + nonce, noncelength, // Nonce + aad, aadLength, // AAD + ciphertext, // input ciphertext + decrypted, // output plaintext + mic, sizeof(mic) // Message Integrity Check + ); + mbedtls_ccm_free(&ctx); + + if (ret == 0) { + Log.notice(F("[BLEDecryptor] Decryption successful" CR)); + } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { + Log.error(F("[BLEDecryptor] Authentication failed." CR)); + return; + } else { + Log.error(F("[BLEDecryptor] Decryption failed with error: %X" CR), ret); + return; + } + + // Build new servicedata + if (BLEdata["encr"].as() == 1){ // PVVX + BLEdata["servicedata"] = NimBLEUtils::dataToHexString(decrypted, ciphertextlen); + } else if (BLEdata["encr"].as() == 2) { // BTHomeV2 + // Build new servicedata + uint8_t newservicedata[3 + ciphertextlen]; + newservicedata[0] = 0x40; // Decrypted BTHomeV2 Packet Type + newservicedata[1] = 0x00; // Packet counter which the PVVX BTHome non-encrypted has but the encrypted does not + newservicedata[2] = 0x00; // **TODO Convert the ctr to the packet counter or just stick with 0? + memcpy(&newservicedata[3], decrypted, ciphertextlen); + BLEdata["servicedata"] = NimBLEUtils::dataToHexString(newservicedata, ciphertextlen + 3); + } else { + return; + } + Log.trace(F("[BLEDecryptor] Decrypted servicedata %s" CR), BLEdata["servicedata"].as()); + + } else if (BLEdata["encr"].as() == 3) { + // Decrypt Victron Energy encrypted advertisements. + size_t nc_off = 0; + uint8_t stream_block[16] = {0}; + + mbedtls_aes_context ctx; + mbedtls_aes_init(&ctx); + mbedtls_aes_setkey_enc(&ctx, bleaeskey, 128); + int ret = mbedtls_aes_crypt_ctr( + &ctx, // AES Key + ciphertextlen, // length of ciphertext + &nc_off, + nonce, // 16 byte nonce with 2 bytes iv + stream_block, + ciphertext, // input ciphertext + decrypted // output plaintext + ); + mbedtls_aes_free(&ctx); + + if (ret == 0) { + Log.notice(F("[BLEDecryptor] Victron Decryption successful" CR)); + } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { + Log.error(F("[BLEDecryptor] Victron Authentication failed." CR)); + return; + } else { + Log.error(F("[BLEDecryptor] Victron decryption failed with error: %X" CR), ret); + return; + } + + // Build new manufacturerdata + unsigned char manufacturerdata[10 + ciphertextlen]; + int manufacturerdatalen = hexToBytes(BLEdata["manufacturerdata"].as(), manufacturerdata, 10); + manufacturerdata[2] = 0x11; // Replace byte 2 with "11" indicate decrypted data + manufacturerdata[7] = 0xff; // Replace byte 7 with "ff" to indicate decrypted data + manufacturerdata[8] = 0xff; // Replace byte 8 with "ff" to indicate decrypted data + memcpy(&manufacturerdata[8], decrypted, ciphertextlen); // Append the decrypted payload to the manufacturer data + BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString(manufacturerdata, 10 + ciphertextlen); // Rebuild manufacturerdata + Log.trace(F("[BLEDecryptor] Victron decrypted manufacturerdata %s" CR), BLEdata["manufacturerdata"].as()); + } + + // Print before and after decoder post decryption + // serializeJsonPretty(BLEdata, Serial); + model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); + // serializeJsonPretty(BLEdata, Serial); + Log.trace(F("[BLEDecryptor] Decrypted model_id %d" CR), model_id); + + // Remove the cipher fields from BLEdata + BLEdata.remove("encr"); + BLEdata.remove("cipher"); + BLEdata.remove("ctr"); + BLEdata.remove("mic"); + + } +# endif + + // Convert prmacs to RMACS until or if OMG gets Identity MAC/IRK decoding + if (BLEdata["prmac"]) { + BLEdata.remove("prmac"); + if (BLEdata["track"]) { + BLEdata.remove("track"); + } + BLEdata["type"] = "RMAC"; + Log.trace(F("Potential RMAC (prmac) converted to RMAC" CR)); + } + const char* deviceName = BLEdata["name"] | ""; + + 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 + 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 + createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); + } else { + createOrUpdateDevice(mac, device_flags_init, model_id, mac_type, deviceName); + if (BTConfig.adaptiveScan == true && (BTConfig.BLEinterval != MinTimeBtwScan || BTConfig.intervalActiveScan != MinTimeBtwScan)) { + if (BLEdata.containsKey("acts") && BLEdata.containsKey("cont")) { + if (BLEdata["acts"] && BLEdata["cont"]) { + BTConfig.BLEinterval = MinTimeBtwScan; + BTConfig.intervalActiveScan = MinTimeBtwScan; + BTConfig.scanDuration = MinScanDuration; + Log.notice(F("Active and continuous scanning required, parameters adapted" CR)); + // stateBTMeasures(false); + } + } else if (BLEdata.containsKey("cont") && BTConfig.BLEinterval != MinTimeBtwScan) { + if (BLEdata["cont"]) { + BTConfig.BLEinterval = MinTimeBtwScan; + if ((BLEdata["type"].as()).compare("CTMO") == 0) { + BTConfig.scanDuration = MinScanDuration; + } + Log.notice(F("Passive continuous scanning required, parameters adapted" CR)); + // stateBTMeasures(false); + } + } + } + } + } else { + if (BLEdata.containsKey("name")) { // Connectable only devices + std::string name = BLEdata["name"]; + if (name.compare("LYWSD03MMC") == 0) + model_id = BLEconectable::id::LYWSD03MMC; + else if (name.compare("DT24-BLE") == 0) + model_id = BLEconectable::id::DT24_BLE; + else if (name.compare("MHO-C401") == 0) + model_id = BLEconectable::id::MHO_C401; + else if (name.compare("XMWSDJ04MMC") == 0) + model_id = BLEconectable::id::XMWSDJ04MMC; + + if (model_id > 0) { + Log.trace(F("Connectable device found: %s" CR), name.c_str()); + createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); + } + } else if (BTConfig.extDecoderEnable && model_id < 0 && BLEdata.containsKey("servicedata")) { + const char* service_data = (const char*)(BLEdata["servicedata"] | ""); + if (strstr(service_data, "209800") != NULL) { + model_id = TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC; + Log.trace(F("Connectable device found: HHCCJCY01HHCC" CR)); + createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); + } + } + } + } else { + Log.trace(F("Random MAC or iBeacon device filtered" CR)); + } + if (!BTConfig.extDecoderEnable && model_id < 0) { + Log.trace(F("No eligible device found " CR)); + } +} +void PublishDeviceData(JsonObject& BLEdata) { + if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough + // Decode the payload + process_bledata(BLEdata); + // If the device is a random MAC and pubRandomMACs is false we don't publish this payload + if (!BTConfig.pubRandomMACs && (BLEdata["type"].as()).compare("RMAC") == 0) { + Log.trace(F("Random MAC, device filtered" CR)); + return; + } + // If pubAdvData is false we don't publish the adv data + if (!BTConfig.pubAdvData) { + BLEdata.remove("servicedatauuid"); + BLEdata.remove("servicedata"); + BLEdata.remove("manufacturerdata"); + BLEdata.remove("mac_type"); + BLEdata.remove("adv_type"); + // tag device properties + // BLEdata.remove("type"); type is used by the WebUI module to determine the template used to display the signal + BLEdata.remove("cidc"); + BLEdata.remove("acts"); + BLEdata.remove("cont"); + BLEdata.remove("track"); + BLEdata.remove("ctrl"); + } + // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid + if (BLEdata.containsKey("distance")) { + if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { + BLEdata["mac"] = BLEdata["id"].as(); + BLEdata["id"] = BLEdata["uuid"].as(); + } + String topic = String(mqtt_topic) + BTConfig.presenceTopic + String(gateway_name); + Log.trace(F("Pub HA Presence %s" CR), topic.c_str()); + BLEdata["topic"] = topic; + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } + + // If the device is not a sensor and pubOnlySensors is true we don't publish this payload + if (!BTConfig.pubOnlySensors || BLEdata.containsKey("model") || !BLEDecoder) { // Identified device + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } else { + Log.notice(F("Not a sensor device filtered" CR)); + return; + } + +# if BLEDecoder + if (enableMultiGTWSync && BLEdata.containsKey("model_id") && BLEdata.containsKey("id")) { + // Publish tracker sync message + bool isTracker = false; + std::string tag = decoder.getTheengAttribute(BLEdata["model_id"].as(), "tag"); + if (tag.length() >= 4) { + isTracker = checkIfIsTracker(tag[3]); + } + + if (isTracker) { + StaticJsonDocument BLEdataBuffer; + JsonObject TrackerSyncdata = BLEdataBuffer.to(); + TrackerSyncdata["gatewayid"] = gateway_name; + TrackerSyncdata["trackerid"] = BLEdata["id"].as(); + String topic = String(mqtt_topic) + String(subjectTrackerSync); + TrackerSyncdata["topic"] = topic.c_str(); + enqueueJsonObject(TrackerSyncdata); + } + } +# endif + } else { + Log.notice(F("Low rssi, device filtered" CR)); + return; + } +} +# else +void process_bledata(JsonObject& BLEdata) {} +void PublishDeviceData(JsonObject& BLEdata) { + if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough + // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid + if (BLEdata.containsKey("distance")) { + if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { + BLEdata["mac"] = BLEdata["id"].as(); + BLEdata["id"] = BLEdata["uuid"].as(); + } + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } else { + Log.notice(F("Low rssi, device filtered" CR)); + return; + } +} +# endif + +void hass_presence(JsonObject& HomePresence) { + int BLErssi = HomePresence["rssi"]; + Log.trace(F("BLErssi %d" CR), BLErssi); + int txPower = HomePresence["txpower"] | 0; + if (txPower >= 0) + txPower = -59; //if tx power is not found we set a default calibration value + Log.trace(F("TxPower: %d" CR), txPower); + double ratio = BLErssi * 1.0 / txPower; + double distance; + if (ratio < 1.0) { + distance = pow(ratio, 10); + } else { + distance = (0.89976) * pow(ratio, 7.7095) + 0.111; + } + HomePresence["distance"] = distance; + Log.trace(F("Ble distance %D" CR), distance); +} + +void BTforceScan() { + if (!BTProcessLock) { + BLEscan(); + Log.trace(F("Scan done" CR)); + if (BTConfig.bleConnect) + BLEconnect(); + } else { + Log.trace(F("Cannot launch scan due to other process running" CR)); + } +} + +void immediateBTAction(void* pvParameters) { + if (BLEactions.size()) { + // Immediate action; we need to prevent the normal connection action and stop scanning + BTProcessLock = true; + NimBLEScan* pScan = NimBLEDevice::getScan(); + if (pScan->isScanning()) { + pScan->stop(); + } + + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { + // swap the vectors so only this device is processed + std::vector dev_swap; + dev_swap.push_back(getDeviceByMac(BLEactions.back().addr.toString().c_str())); + std::swap(devices, dev_swap); + + std::vector act_swap; + act_swap.push_back(BLEactions.back()); + BLEactions.pop_back(); + std::swap(BLEactions, act_swap); + + // Unlock here to allow the action to be performed + BTProcessLock = false; + BLEconnect(); + // back to normal + std::swap(devices, dev_swap); + std::swap(BLEactions, act_swap); + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + } else { + Log.error(F("CreateOrUpdate Semaphore NOT taken" CR)); + } + + // If we stopped the scheduled connect for this action, do the scheduled now + if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { + timeBetweenConnect = millis(); + BLEconnect(); + } + xSemaphoreGive(semaphoreBLEOperation); + } else { + Log.error(F("BLE busy - immediateBTAction not sent" CR)); + gatewayState = GatewayState::ERROR; + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = BLEactions.back().addr.toString(); + BLEdata["success"] = false; + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + BLEactions.pop_back(); + BTProcessLock = false; + } + } + vTaskDelete(NULL); +} + +void startBTActionTask() { + TaskHandle_t th; + xTaskCreateUniversal( + immediateBTAction, /* Function to implement the task */ + "imActTask", /* Name of the task */ + 8000, /* Stack size in bytes */ + NULL, /* Task input parameter */ + 3, /* Priority of the task (set higher than core task) */ + &th, /* Task handle. */ + 1); /* Core where the task should run */ +} + +# if BLEDecoder +void KnownBTActions(JsonObject& BTdata) { + if (!BTdata.containsKey("id")) { + Log.error(F("BLE mac address missing" CR)); + gatewayState = GatewayState::ERROR; + return; + } + + BLEAction action{}; + action.write = true; + action.ttl = 3; + bool res = false; + if (BTdata.containsKey("model_id") && BTdata["model_id"].is()) { + if (BTdata["model_id"] == "X1") { + if (BTdata.containsKey("cmd") && BTdata["cmd"].is()) { + action.value_type = BLE_VAL_STRING; + std::string val = BTdata["cmd"].as(); // Fix #1694 + action.value = val; + createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBS1, 1); + res = true; + } + } else if (BTdata["model_id"] == "W270160X") { + if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { + action.value_type = BLE_VAL_INT; + res = true; + } else if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { + action.value_type = BLE_VAL_STRING; + res = true; + } + if (res) { + std::string val = BTdata["tilt"].as(); // Fix #1694 + action.value = val; + createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBBT, 1); + } + } else if (BTdata["model_id"] == "W070160X") { + if (BTdata.containsKey("position") && BTdata["position"].is()) { + action.value_type = BLE_VAL_INT; + res = true; + } else if (BTdata.containsKey("position") && BTdata["position"].is()) { + action.value_type = BLE_VAL_STRING; + res = true; + } + if (res) { + std::string val = BTdata["position"].as(); // Fix #1694 + action.value = val; + createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBCU, 1); + } + } + if (res) { + action.addr = NimBLEAddress(BTdata["id"].as(), 1); + BLEactions.push_back(action); + startBTActionTask(); + } else { + Log.error(F("BLE action not recognized" CR)); + gatewayState = GatewayState::ERROR; + } + } +} +# else +void KnownBTActions(JsonObject& BTdata) {} +# endif + +void XtoBTAction(JsonObject& BTdata) { + BLEAction action{}; + action.ttl = BTdata.containsKey("ttl") ? (uint8_t)BTdata["ttl"] : 1; + action.value_type = BLE_VAL_STRING; + if (BTdata.containsKey("value_type")) { + String vt = BTdata["value_type"]; + vt.toUpperCase(); + if (vt == "HEX") + action.value_type = BLE_VAL_HEX; + else if (vt == "INT") + action.value_type = BLE_VAL_INT; + else if (vt == "FLOAT") + action.value_type = BLE_VAL_FLOAT; + else if (vt != "STRING") { + Log.error(F("BLE value type invalid %s" CR), vt.c_str()); + return; + } + } + + Log.trace(F("BLE ACTION TTL = %u" CR), action.ttl); + action.complete = false; + if (BTdata.containsKey("ble_write_address") && + BTdata.containsKey("ble_write_service") && + BTdata.containsKey("ble_write_char") && + BTdata.containsKey("ble_write_value")) { + action.addr = NimBLEAddress(BTdata["ble_write_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); + action.service = NimBLEUUID((const char*)BTdata["ble_write_service"]); + action.characteristic = NimBLEUUID((const char*)BTdata["ble_write_char"]); + std::string val = BTdata["ble_write_value"].as(); // Fix #1694 + action.value = val; + action.write = true; + Log.trace(F("BLE ACTION Write" CR)); + } else if (BTdata.containsKey("ble_read_address") && + BTdata.containsKey("ble_read_service") && + BTdata.containsKey("ble_read_char")) { + action.addr = NimBLEAddress(BTdata["ble_read_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); + action.service = NimBLEUUID((const char*)BTdata["ble_read_service"]); + action.characteristic = NimBLEUUID((const char*)BTdata["ble_read_char"]); + action.write = false; + Log.trace(F("BLE ACTION Read" CR)); + } else { + return; + } + + createOrUpdateDevice(action.addr.toString().c_str(), device_flags_connect, UNKWNON_MODEL, action.addr.getType()); + + BLEactions.push_back(action); + if (BTdata.containsKey("immediate") && BTdata["immediate"].as()) { + startBTActionTask(); + } +} + +void XtoBT(const char* topicOri, JsonObject& BTdata) { // json object decoding + if (cmpToMainTopic(topicOri, subjectMQTTtoBTset)) { + Log.trace(F("MQTTtoBT json set" CR)); + + // Black list & white list set + bool WorBupdated; + WorBupdated = updateWorB(BTdata, true); + WorBupdated |= updateWorB(BTdata, false); + + if (WorBupdated) { + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { + //dumpDevices(); + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + } + } + + // Force scan now + if (BTdata.containsKey("interval") && BTdata["interval"] == 0) { + Log.notice(F("BLE forced scan" CR)); + atomic_store_explicit(&forceBTScan, 1, ::memory_order_seq_cst); // ask the other core to do the scan for us + } + + /* + * Configuration modifications priorities: + * First `init=true` and `load=true` commands are executed (if both are present, INIT prevails on LOAD) + * Then parameters included in json are taken in account + * Finally `erase=true` and `save=true` commands are executed (if both are present, ERASE prevails on SAVE) + */ + if (BTdata.containsKey("init") && BTdata["init"].as()) { + // Restore the default (initial) configuration + BTConfig_init(); + } else if (BTdata.containsKey("load") && BTdata["load"].as()) { + // Load the saved configuration, if not initialised + BTConfig_load(); + } + + // Load config from json if available + BTConfig_fromJson(BTdata); + + } else if (cmpToMainTopic(topicOri, subjectMQTTtoBT)) { + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { + KnownBTActions(BTdata); + XtoBTAction(BTdata); + xSemaphoreGive(semaphoreBLEOperation); + } else { + Log.error(F("BLE busy - BTActions not sent" CR)); + gatewayState = GatewayState::ERROR; + } + } else if (strstr(topicOri, subjectTrackerSync) != NULL) { + if (BTdata.containsKey("gatewayid") && BTdata.containsKey("trackerid") && BTdata["gatewayid"] != gateway_name) { + BLEdevice* device = getDeviceByMac(BTdata["trackerid"].as()); + if (device != &NO_BT_DEVICE_FOUND && device->lastUpdate != 0) { + device->lastUpdate = 0; + Log.notice(F("Tracker %s disassociated by gateway %s" CR), BTdata["trackerid"].as(), BTdata["gatewayid"].as()); + } + } + } +} +#endif From 43280c99ccda3d222c74f545a5091e6575e05551 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 10 Aug 2025 15:50:12 +1200 Subject: [PATCH 17/24] Fix typo with entity_name for device discovery --- main/gatewayBT.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index f2a6fc83b8..e76e8d4e80 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -1018,7 +1018,7 @@ 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::LYWSD03MMC_ATC && p->sensorModel_id <= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_PVVX_BTHOME_2 ) { + if (displayDeviceName && p->sensorModel_id >= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_ATC && p->sensorModel_id <= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_PVVX_BTHOME_2 ) { xxWSD0xMMCDiscovery(macWOdots.c_str(), p->name, model_id.c_str()); } else if (!properties.empty()) { StaticJsonDocument jsonBuffer; @@ -1044,7 +1044,6 @@ void launchBTDiscovery(bool overrideDiscovery) { } else { entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); } - String entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); String unique_id = macWOdots + "-" + String(prop.key().c_str()); String value_template = "{{ value_json." + String(prop.key().c_str()) + " | is_defined }}"; if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1 && strcmp(prop.key().c_str(), "state") == 0) { From 5587c492eda9d7117628b4fa4545326d7a1ab371 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 10 Aug 2025 16:02:40 +1200 Subject: [PATCH 18/24] Add support for LYWSD03MMC as a DiscoveryFromList --- main/gatewayBT.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index e76e8d4e80..30ae942adc 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -593,9 +593,9 @@ void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorMo }; if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) { const char* power[9] = {"sensor", "Power", mac, "", jsonPower, "", "", "", stateClassNone}; - memcpy(xxWSD0xMMCsensor[4], power, 9); + memcpy(xxWSD0xMMCsensor[5], power, 9); const char* open[9] = {"sensor", "Opening", mac, "", jsonOpen, "", "", "", stateClassNone}; - memcpy(xxWSD0xMMCsensor[5], open, 9); + memcpy(xxWSD0xMMCsensor[6], open, 9); } createDiscoveryFromList(mac, xxWSD0xMMCsensor, xxWSD0xMMCparametersCount, name, "Xiaomi", sensorModel); } From 471839c510269220b107aa98dc055e10ac8c2896 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 10 Aug 2025 16:20:15 +1200 Subject: [PATCH 19/24] Add support for LYWSD03MMC as a DiscoveryFromList --- main/gatewayBT.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index 30ae942adc..47c8788586 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -593,9 +593,9 @@ void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorMo }; if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) { const char* power[9] = {"sensor", "Power", mac, "", jsonPower, "", "", "", stateClassNone}; - memcpy(xxWSD0xMMCsensor[5], power, 9); + memcpy(&xxWSD0xMMCsensor[5], power, sizeof(power)); const char* open[9] = {"sensor", "Opening", mac, "", jsonOpen, "", "", "", stateClassNone}; - memcpy(xxWSD0xMMCsensor[6], open, 9); + memcpy(&xxWSD0xMMCsensor[6], open, sizeof(open)); } createDiscoveryFromList(mac, xxWSD0xMMCsensor, xxWSD0xMMCparametersCount, name, "Xiaomi", sensorModel); } From f63ccbf95d4b69ede99b11f7cbbca8228ba1887b Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 10 Aug 2025 19:26:06 +1200 Subject: [PATCH 20/24] Adjust voltage to not come in PVVX ENCR payloads --- main/gatewayBT.cpp | 3728 ++++++++++++++++++++-------------------- main/mqttDiscovery.cpp | 3135 ++++++++++++++++----------------- 2 files changed, 3434 insertions(+), 3429 deletions(-) diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index 47c8788586..f310deeffa 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -1,1862 +1,1866 @@ -/* - OpenMQTTGateway - ESP8266 or Arduino program for home automation - - Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker - Send and receiving command by MQTT - - This gateway enables to: - - publish MQTT data to a topic related to BLE devices data - - Copyright: (c)Florian ROBERT - - This file is part of OpenMQTTGateway. - - OpenMQTTGateway is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - OpenMQTTGateway is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Thanks to wolass https://github.com/wolass for suggesting me HM 10 and dinosd https://github.com/dinosd/BLE_PROXIMITY for inspiring me how to implement the gateway -*/ -#include "User_config.h" - -#ifdef ZgatewayBT -# include "TheengsCommon.h" - -SemaphoreHandle_t semaphoreCreateOrUpdateDevice; -SemaphoreHandle_t semaphoreBLEOperation; -QueueHandle_t BLEQueue; -unsigned long scanCount = 0; -# include -# include -# include -# include -# include -# include - -# include - -# include "TheengsCommon.h" -# include "config_mqttDiscovery.h" -# include "gatewayBLEConnect.h" -# include "soc/timer_group_reg.h" -# include "soc/timer_group_struct.h" - -using namespace std; - -// Global struct to store live BT configuration data -BTConfig_s BTConfig; - -# if BLEDecoder -# include -# if BLEDecryptor -# include "mbedtls/ccm.h" -# include "mbedtls/aes.h" -# endif -TheengsDecoder decoder; -# endif - -static TaskHandle_t xCoreTaskHandle; -static TaskHandle_t xProcBLETaskHandle; - -struct decompose { - int start; - int len; - bool reverse; -}; - -vector BLEactions; - -vector devices; -int newDevices = 0; - -static BLEdevice NO_BT_DEVICE_FOUND = { - {0}, - 0, - false, - false, - false, - false, - (char)UNKWNON_MODEL, - 0, -}; -static bool oneWhite = false; - -extern bool BTProcessLock; -extern int queueLength; - -void setupBTTasksAndBLE(); -bool checkIfIsTracker(char ch); -void hass_presence(JsonObject& HomePresence); -void BTforceScan(); - -void BTConfig_init() { - BTConfig.bleConnect = AttemptBLEConnect; - BTConfig.BLEinterval = TimeBtwRead; - BTConfig.adaptiveScan = AdaptiveBLEScan; - BTConfig.intervalActiveScan = TimeBtwActive; - BTConfig.intervalConnect = TimeBtwConnect; - BTConfig.scanDuration = Scan_duration; - BTConfig.pubOnlySensors = PublishOnlySensors; - BTConfig.pubRandomMACs = PublishRandomMACs; - BTConfig.presenceEnable = HassPresence; - BTConfig.presenceTopic = subjectHomePresence; - BTConfig.presenceUseBeaconUuid = useBeaconUuidForPresence; - BTConfig.minRssi = MinimumRSSI; - BTConfig.extDecoderEnable = UseExtDecoder; - BTConfig.extDecoderTopic = MQTTDecodeTopic; - BTConfig.filterConnectable = BLE_FILTER_CONNECTABLE; - BTConfig.pubAdvData = pubBLEAdvData; - BTConfig.pubBeaconUuidForTopic = useBeaconUuidForTopic; - BTConfig.ignoreWBlist = false; - BTConfig.presenceAwayTimer = PresenceAwayTimer; - BTConfig.movingTimer = MovingTimer; - BTConfig.forcePassiveScan = false; - BTConfig.enabled = EnableBT; -} - -unsigned long timeBetweenConnect = 0; -unsigned long timeBetweenActive = 0; - -String stateBTMeasures(bool start) { - StaticJsonDocument jsonBuffer; - JsonObject jo = jsonBuffer.to(); - jo["bleconnect"] = BTConfig.bleConnect; - jo["interval"] = BTConfig.BLEinterval; - jo["adaptivescan"] = BTConfig.adaptiveScan; - jo["intervalacts"] = BTConfig.intervalActiveScan; - jo["intervalcnct"] = BTConfig.intervalConnect; - jo["scanduration"] = BTConfig.scanDuration; - jo["hasspresence"] = BTConfig.presenceEnable; - jo["prestopic"] = BTConfig.presenceTopic; - jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; - jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value - jo["extDecoderEnable"] = BTConfig.extDecoderEnable; - jo["extDecoderTopic"] = BTConfig.extDecoderTopic; - jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; - jo["ignoreWBlist"] = BTConfig.ignoreWBlist; - jo["forcepscn"] = BTConfig.forcePassiveScan; - jo["tskstck"] = uxTaskGetStackHighWaterMark(xProcBLETaskHandle); - jo["crstck"] = uxTaskGetStackHighWaterMark(xCoreTaskHandle); - jo["enabled"] = BTConfig.enabled; - jo["scnct"] = scanCount; -# if BLEDecoder - jo["onlysensors"] = BTConfig.pubOnlySensors; - jo["randommacs"] = BTConfig.pubRandomMACs; - jo["filterConnectable"] = BTConfig.filterConnectable; - jo["pubadvdata"] = BTConfig.pubAdvData; - jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; - jo["movingtimer"] = BTConfig.movingTimer; -# endif - - if (start) { - Log.notice(F("BT sys: ")); - serializeJsonPretty(jsonBuffer, Serial); - Serial.println(); - return ""; // Do not try to erase/write/send config at startup - } - String output; - serializeJson(jo, output); - jo["origin"] = subjectBTtoMQTT; - enqueueJsonObject(jo, QueueSemaphoreTimeOutTask); - return (output); -} - -void BTConfig_fromJson(JsonObject& BTdata, bool startup = false) { - // Attempts to connect to eligible devices or not - Config_update(BTdata, "bleconnect", BTConfig.bleConnect); - // Identify AdaptiveScan deactivation to pass to continuous mode or activation to come back to default settings - if (startup == false) { - if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == false && BTConfig.presenceEnable == true) { - BTdata["adaptivescan"] = true; - } else if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == true && BTConfig.presenceEnable == false) { - BTdata["adaptivescan"] = false; - } - - if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == false && BTConfig.adaptiveScan == true) { - BTdata["interval"] = MinTimeBtwScan; - BTdata["intervalacts"] = MinTimeBtwScan; - BTdata["scanduration"] = MinScanDuration; - } else if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == true && BTConfig.adaptiveScan == false) { - BTdata["interval"] = TimeBtwRead; - BTdata["intervalacts"] = TimeBtwActive; - BTdata["scanduration"] = Scan_duration; - } - // Identify if the gateway is enabled or not and stop start accordingly - if (BTdata.containsKey("enabled") && BTdata["enabled"] == false && BTConfig.enabled == true) { - // Stop the gateway but without deinit to enable a future BT restart - stopProcessing(false); - } else if (BTdata.containsKey("enabled") && BTdata["enabled"] == true && BTConfig.enabled == false) { - BTProcessLock = false; - setupBTTasksAndBLE(); - } - } - // Home Assistant presence message - Config_update(BTdata, "hasspresence", BTConfig.presenceEnable); - // Time before before active scan - // Scan interval set - and avoid intervalacts to be lower than interval - if (BTdata.containsKey("interval") && BTdata["interval"] != 0) { - BTConfig.adaptiveScan = false; - Config_update(BTdata, "interval", BTConfig.BLEinterval); - if (BTConfig.intervalActiveScan < BTConfig.BLEinterval) { - Config_update(BTdata, "interval", BTConfig.intervalActiveScan); - } - } - // Define if the scan is adaptive or not - and avoid intervalacts to be lower than interval - if (BTdata.containsKey("intervalacts") && BTdata["intervalacts"] < BTConfig.BLEinterval) { - BTConfig.adaptiveScan = false; - // Config_update(BTdata, "interval", BTConfig.intervalActiveScan); - BTConfig.intervalActiveScan = BTConfig.BLEinterval; - } else { - Config_update(BTdata, "intervalacts", BTConfig.intervalActiveScan); - } - // Adaptive scan set - Config_update(BTdata, "adaptivescan", BTConfig.adaptiveScan); - // Time before a connect set - Config_update(BTdata, "intervalcnct", BTConfig.intervalConnect); - // publish all BLE devices discovered or only the identified sensors (like temperature sensors) - Config_update(BTdata, "scanduration", BTConfig.scanDuration); - // define the duration for a scan; in milliseconds - Config_update(BTdata, "onlysensors", BTConfig.pubOnlySensors); - // publish devices which randomly change their MAC addresses - Config_update(BTdata, "randommacs", BTConfig.pubRandomMACs); - // Home Assistant presence message topic - Config_update(BTdata, "prestopic", BTConfig.presenceTopic); - // Home Assistant presence message use iBeacon UUID - Config_update(BTdata, "presuseuuid", BTConfig.presenceUseBeaconUuid); - // Timer to trigger a device state as offline if not seen - Config_update(BTdata, "presenceawaytimer", BTConfig.presenceAwayTimer); - // Timer to trigger a device state as offline if not seen - Config_update(BTdata, "movingtimer", BTConfig.movingTimer); - // Force passive scan - Config_update(BTdata, "forcepscn", BTConfig.forcePassiveScan); - // MinRSSI set - Config_update(BTdata, "minrssi", BTConfig.minRssi); - // Send undecoded device data - Config_update(BTdata, "extDecoderEnable", BTConfig.extDecoderEnable); - // Topic to send undecoded device data - Config_update(BTdata, "extDecoderTopic", BTConfig.extDecoderTopic); - // Sets whether to filter publishing - Config_update(BTdata, "filterConnectable", BTConfig.filterConnectable); - // Publish advertisement data - Config_update(BTdata, "pubadvdata", BTConfig.pubAdvData); - // Use iBeacon UUID as topic, instead of sender (random) MAC address - Config_update(BTdata, "pubuuid4topic", BTConfig.pubBeaconUuidForTopic); - // Disable Whitelist & Blacklist - Config_update(BTdata, "ignoreWBlist", (BTConfig.ignoreWBlist)); - // Enable or disable the BT gateway - Config_update(BTdata, "enabled", BTConfig.enabled); - - stateBTMeasures(startup); - - if (BTdata.containsKey("erase") && BTdata["erase"].as()) { - // Erase config from NVS (non-volatile storage) - preferences.begin(Gateway_Short_Name, false); - if (preferences.isKey("BTConfig")) { - int result = preferences.remove("BTConfig"); - Log.notice(F("BT config erase result: %d" CR), result); - preferences.end(); - return; // Erase prevails on save, so skipping save - } else { - preferences.end(); - Log.notice(F("BT config not found" CR)); - } - } - - if (BTdata.containsKey("save") && BTdata["save"].as()) { - StaticJsonDocument jsonBuffer; - JsonObject jo = jsonBuffer.to(); - jo["bleconnect"] = BTConfig.bleConnect; - jo["interval"] = BTConfig.BLEinterval; - jo["adaptivescan"] = BTConfig.adaptiveScan; - jo["intervalacts"] = BTConfig.intervalActiveScan; - jo["intervalcnct"] = BTConfig.intervalConnect; - jo["scanduration"] = BTConfig.scanDuration; - jo["onlysensors"] = BTConfig.pubOnlySensors; - jo["randommacs"] = BTConfig.pubRandomMACs; - jo["hasspresence"] = BTConfig.presenceEnable; - jo["prestopic"] = BTConfig.presenceTopic; - jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; - jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value - jo["extDecoderEnable"] = BTConfig.extDecoderEnable; - jo["extDecoderTopic"] = BTConfig.extDecoderTopic; - jo["filterConnectable"] = BTConfig.filterConnectable; - jo["pubadvdata"] = BTConfig.pubAdvData; - jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; - jo["ignoreWBlist"] = BTConfig.ignoreWBlist; - jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; - jo["movingtimer"] = BTConfig.movingTimer; - jo["forcepscn"] = BTConfig.forcePassiveScan; - jo["enabled"] = BTConfig.enabled; - // Save config into NVS (non-volatile storage) - String conf = ""; - serializeJson(jsonBuffer, conf); - preferences.begin(Gateway_Short_Name, false); - int result = preferences.putString("BTConfig", conf); - preferences.end(); - Log.notice(F("BT config save: %s, result: %d" CR), conf.c_str(), result); - } -} - -void BTConfig_load() { - StaticJsonDocument jsonBuffer; - preferences.begin(Gateway_Short_Name, true); - if (preferences.isKey("BTConfig")) { - auto error = deserializeJson(jsonBuffer, preferences.getString("BTConfig", "{}")); - preferences.end(); - Log.notice(F("BT config loaded" CR)); - if (error) { - Log.error(F("BT config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity()); - return; - } - if (jsonBuffer.isNull()) { - Log.warning(F("BT config is null" CR)); - return; - } - JsonObject jo = jsonBuffer.as(); - BTConfig_fromJson(jo, true); // Never send MQTT message with config - Log.notice(F("BT config loaded" CR)); - } else { - preferences.end(); - Log.notice(F("BT config not found" CR)); - } -} - -void PublishDeviceData(JsonObject& BLEdata); - -atomic_int forceBTScan; - -void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type = 0, const char* name = ""); - -BLEdevice* getDeviceByMac(const char* mac); // Declared here to avoid pre-compilation issue (misplaced auto declaration by pio) -BLEdevice* getDeviceByMac(const char* mac) { - Log.trace(F("getDeviceByMac %s" CR), mac); - - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - if ((strcmp((*it)->macAdr, mac) == 0)) { - return *it; - } - } - return &NO_BT_DEVICE_FOUND; -} - -bool updateWorB(JsonObject& BTdata, bool isWhite) { - Log.trace(F("update WorB" CR)); - const char* jsonKey = isWhite ? "white-list" : "black-list"; - - int size = BTdata[jsonKey].size(); - if (size == 0) - return false; - - for (int i = 0; i < size; i++) { - const char* mac = BTdata[jsonKey][i]; - createOrUpdateDevice(mac, (isWhite ? device_flags_isWhiteL : device_flags_isBlackL), - UNKWNON_MODEL); - } - - return true; -} - -void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type, const char* name) { - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(30000)) == pdFALSE) { - Log.error(F("Semaphore NOT taken" CR)); - return; - } - BLEdevice* device = getDeviceByMac(mac); - if (device == &NO_BT_DEVICE_FOUND) { - Log.trace(F("add %s" CR), mac); - //new device - device = new BLEdevice(); - strcpy(device->macAdr, mac); - device->isDisc = flags & device_flags_isDisc; - device->isWhtL = flags & device_flags_isWhiteL; - device->isBlkL = flags & device_flags_isBlackL; - device->connect = flags & device_flags_connect; - device->macType = mac_type; - // Check name length - if (strlen(name) > 20) { - Log.warning(F("Name too long, truncating" CR)); - strncpy(device->name, name, 20); - device->name[19] = '\0'; - } else { - strcpy(device->name, name); - } - device->sensorModel_id = model; - device->lastUpdate = millis(); - devices.push_back(device); - newDevices++; - } else { - Log.trace(F("update %s" CR), mac); - device->lastUpdate = millis(); - device->macType = mac_type; - - if (flags & device_flags_isDisc) { - device->isDisc = true; - } - - if (flags & device_flags_connect) { - device->connect = true; - } - - if (model != UNKWNON_MODEL && device->sensorModel_id == UNKWNON_MODEL) { - newDevices++; - device->isDisc = false; - device->sensorModel_id = model; - } - - // If a device has been added to the white-list, flag it so it can be auto-detected - if (!device->isWhtL && flags & device_flags_isWhiteL) { - newDevices++; - } - if (flags & device_flags_isWhiteL || flags & device_flags_isBlackL) { - device->isWhtL = flags & device_flags_isWhiteL; - device->isBlkL = flags & device_flags_isBlackL; - } - } - - // update oneWhite flag - oneWhite = oneWhite || device->isWhtL; - - xSemaphoreGive(semaphoreCreateOrUpdateDevice); -} - -void updateDevicesStatus() { - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - BLEdevice* p = *it; - unsigned long now = millis(); - // Check for tracker status - bool isTracker = false; -# if BLEDecoder - std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); - if (tag.length() >= 4) { - isTracker = checkIfIsTracker(tag[3]); - } - // Device tracker devices - if (isTracker) { // We apply the offline status only for tracking device, can be extended further to all the devices - if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.presenceAwayTimer) && (now > BTConfig.presenceAwayTimer)) && - (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = p->macAdr; - BLEdata["state"] = "offline"; - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - // We set the lastUpdate to 0 to avoid replublishing the offline state - p->lastUpdate = 0; - } - } - // Moving detection devices (devices with an accelerometer) - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BC08) { - if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.movingTimer) && (now > BTConfig.movingTimer)) && - (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = p->macAdr; - BLEdata["state"] = "offline"; - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - // We set the lastUpdate to 0 to avoid replublishing the offline state - p->lastUpdate = 0; - } - } -# endif - } -} - -void dumpDevices() { -# if LOG_LEVEL > LOG_LEVEL_NOTICE - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - BLEdevice* p = *it; - Log.trace(F("macAdr %s" CR), p->macAdr); - Log.trace(F("macType %d" CR), p->macType); - Log.trace(F("isDisc %d" CR), p->isDisc); - Log.trace(F("isWhtL %d" CR), p->isWhtL); - Log.trace(F("isBlkL %d" CR), p->isBlkL); - Log.trace(F("connect %d" CR), p->connect); - Log.trace(F("sensorModel_id %d" CR), p->sensorModel_id); - Log.trace(F("LastUpdate %u" CR), p->lastUpdate); - } -# endif -} - -void strupp(char* beg) { - while ((*beg = toupper(*beg))) - ++beg; -} - -# ifdef ZmqttDiscovery -void DT24Discovery(const char* mac, const char* sensorModel_id) { -# define DT24parametersCount 7 - Log.trace(F("DT24Discovery" CR)); - const char* DT24sensor[DT24parametersCount][9] = { - {"sensor", "volt", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "amp", mac, "current", jsonCurrent, "", "", "A", stateClassMeasurement}, - {"sensor", "watt", mac, "power", jsonPower, "", "", "W", stateClassMeasurement}, - {"sensor", "watt-hour", mac, "power", jsonEnergy, "", "", "kWh", stateClassMeasurement}, - {"sensor", "price", mac, "", jsonMsg, "", "", "", stateClassNone}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"binary_sensor", "inUse", mac, "power", jsonInuse, "", "", "", stateClassNone} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, DT24sensor, DT24parametersCount, "DT24", "ATorch", sensorModel_id); -} - -void BM2Discovery(const char* mac, const char* sensorModel_id) { -# define BM2parametersCount 2 - Log.trace(F("BM2Discovery" CR)); - const char* BM2sensor[BM2parametersCount][9] = { - {"sensor", "volt", mac, "voltage", jsonVoltBM2, "", "", "V", stateClassMeasurement}, // We use a json definition that retrieve only data from the BM2 decoder, as this sensor also advertize volt as an iBeacon - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, BM2sensor, BM2parametersCount, "BM2", "Generic", sensorModel_id); -} - -void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) { -# define LYWSD03MMCparametersCount 4 - Log.trace(F("LYWSD03MMCDiscovery" CR)); - const char* LYWSD03MMCsensor[LYWSD03MMCparametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, LYWSD03MMCsensor, LYWSD03MMCparametersCount, "LYWSD03MMC", "Xiaomi", sensorModel); -} - -void MHO_C401Discovery(const char* mac, const char* sensorModel) { -# define MHO_C401parametersCount 4 - Log.trace(F("MHO_C401Discovery" CR)); - const char* MHO_C401sensor[MHO_C401parametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, MHO_C401sensor, MHO_C401parametersCount, "MHO_C401", "Xiaomi", sensorModel); -} - -void HHCCJCY01HHCCDiscovery(const char* mac, const char* sensorModel) { -# define HHCCJCY01HHCCparametersCount 5 - Log.trace(F("HHCCJCY01HHCCDiscovery" CR)); - const char* HHCCJCY01HHCCsensor[HHCCJCY01HHCCparametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "lux", mac, "illuminance", jsonLux, "", "", "lx", stateClassMeasurement}, - {"sensor", "fer", mac, "", jsonFer, "", "", "µS/cm", stateClassMeasurement}, - {"sensor", "moi", mac, "", jsonMoi, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, HHCCJCY01HHCCsensor, HHCCJCY01HHCCparametersCount, "HHCCJCY01HHCC", "Xiaomi", sensorModel); -} - -void XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel) { -# define XMWSDJ04MMCparametersCount 4 - Log.trace(F("XMWSDJ04MMCDiscovery" CR)); - const char* XMWSDJ04MMCsensor[XMWSDJ04MMCparametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, XMWSDJ04MMCsensor, XMWSDJ04MMCparametersCount, "XMWSDJ04MMC", "Xiaomi", sensorModel); -} - -void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) { - Log.trace(F("xxWSD0xMMCDiscovery" CR)); - int xxWSD0xMMCparametersCount = 5; - if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) xxWSD0xMMCparametersCount = 7; - const char* xxWSD0xMMCsensor[xxWSD0xMMCparametersCount][9] = { - {"sensor", "Battery", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "Voltage", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "Temperature", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "Humidity", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement}, - {"sensor", "RSSI", mac, "signal_strength", jsonRSSI, "", "", "dB", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement, state class - }; - if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) { - const char* power[9] = {"sensor", "Power", mac, "", jsonPower, "", "", "", stateClassNone}; - memcpy(&xxWSD0xMMCsensor[5], power, sizeof(power)); - const char* open[9] = {"sensor", "Opening", mac, "", jsonOpen, "", "", "", stateClassNone}; - memcpy(&xxWSD0xMMCsensor[6], open, sizeof(open)); - } - createDiscoveryFromList(mac, xxWSD0xMMCsensor, xxWSD0xMMCparametersCount, name, "Xiaomi", sensorModel); -} - -# else -void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) {} -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 XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel_id) {} -void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) {} -# endif - -/* - Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleScan.cpp - Ported to Arduino ESP32 by Evandro Copercini - */ -// core task implementation thanks to https://techtutorialsx.com/2017/05/09/esp32-running-code-on-a-specific-core/ - -//core on which the BLE detection task will run -static int taskCore = 0; - -class ScanCallbacks : public NimBLEScanCallbacks { - void onResult(const NimBLEAdvertisedDevice* advertisedDevice) { - NimBLEAdvertisedDevice* ad = new NimBLEAdvertisedDevice(*advertisedDevice); - if (xQueueSend(BLEQueue, &ad, 0) != pdTRUE) { - Log.error(F("BLEQueue full" CR)); - delete (ad); - } - } -} scanCallbacks; - -std::string convertServiceData(std::string deviceServiceData) { - int serviceDataLength = (int)deviceServiceData.length(); - char spr[2 * serviceDataLength + 1]; - for (int i = 0; i < serviceDataLength; i++) sprintf(spr + 2 * i, "%.2x", (unsigned char)deviceServiceData[i]); - spr[2 * serviceDataLength] = 0; - Log.trace(F("Converted service data (%d) to %s" CR), serviceDataLength, spr); - return spr; -} - -bool checkIfIsTracker(char ch) { - uint8_t data = 0; - if (ch >= '0' && ch <= '9') - data = ch - '0'; - else if (ch >= 'a' && ch <= 'f') - data = 10 + (ch - 'a'); - - if (((data >> 3) & 0x01) == 1) { - Log.trace(F("Is Device Tracker" CR)); - return true; - } else { - return false; - } -} - -void procBLETask(void* pvParameters) { - BLEAdvertisedDevice* advertisedDevice = nullptr; - - for (;;) { - xQueueReceive(BLEQueue, &advertisedDevice, portMAX_DELAY); - // Feed the watchdog - //esp_task_wdt_reset(); - if (!BTProcessLock) { - Log.trace(F("Creating BLE buffer" CR)); - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = advertisedDevice->getAddress().toString(); - BLEdata["mac_type"] = advertisedDevice->getAddress().getType(); - BLEdata["adv_type"] = advertisedDevice->getAdvType(); - Log.notice(F("BT Device detected: %s" CR), BLEdata["id"].as()); - BLEdevice* device = getDeviceByMac(BLEdata["id"].as()); - - if (BTConfig.filterConnectable && device->connect) { - Log.notice(F("Filtered connectable device" CR)); - delete (advertisedDevice); - continue; - } - - if (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(device)) && !isBlack(device))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC) - if (advertisedDevice->haveName()) - BLEdata["name"] = (char*)advertisedDevice->getName().c_str(); - if (advertisedDevice->haveManufacturerData()) { - BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString((uint8_t*)advertisedDevice->getManufacturerData().data(), - advertisedDevice->getManufacturerData().length()); - } - BLEdata["rssi"] = (int)advertisedDevice->getRSSI(); - if (advertisedDevice->haveTXPower()) - BLEdata["txpower"] = (int8_t)advertisedDevice->getTXPower(); - if (BTConfig.presenceEnable) { - hass_presence(BLEdata); // with either only sensors or not we can use it for home assistant room presence component - } - if (advertisedDevice->haveServiceData()) { - int serviceDataCount = advertisedDevice->getServiceDataCount(); - Log.trace(F("Get services data number: %d" CR), serviceDataCount); - for (int j = 0; j < serviceDataCount; j++) { - StaticJsonDocument BLEdataBufferTemp; - JsonObject BLEdataTemp = BLEdataBufferTemp.to(); - BLEdataBufferTemp = BLEdataBuffer; - std::string service_data = convertServiceData(advertisedDevice->getServiceData(j)); - Log.trace(F("Service data: %s" CR), service_data.c_str()); - std::string serviceDatauuid = advertisedDevice->getServiceDataUUID(j).toString(); - Log.trace(F("Service data UUID: %s" CR), (char*)serviceDatauuid.c_str()); - BLEdataTemp["servicedata"] = (char*)service_data.c_str(); - BLEdataTemp["servicedatauuid"] = (char*)serviceDatauuid.c_str(); - PublishDeviceData(BLEdataTemp); - } - } else { - PublishDeviceData(BLEdata); - } - } else { - Log.trace(F("Filtered MAC device" CR)); - } - updateDevicesStatus(); - } - delete (advertisedDevice); - vTaskDelay(10); - } -} - -/** - * BLEscan used to retrieve BLE advertized data from devices without connection - */ -void BLEscan() { - // Don't start the next scan until processing of previous results is complete. - while (uxQueueMessagesWaiting(BLEQueue) || queueLength != 0) { // the criteria on queueLength could be adjusted to parallelize the scan and the queue processing - delay(1); // Wait for queue to empty, a yield here instead of the delay cause the WDT to trigger - } - Log.notice(F("Scan begin" CR)); - BLEScan* pBLEScan = BLEDevice::getScan(); - pBLEScan->setScanCallbacks(&scanCallbacks); - if ((millis() > (timeBetweenActive + BTConfig.intervalActiveScan) || BTConfig.intervalActiveScan == BTConfig.BLEinterval) && !BTConfig.forcePassiveScan) { - pBLEScan->setActiveScan(true); - timeBetweenActive = millis(); - } else { - pBLEScan->setActiveScan(false); - } - pBLEScan->setInterval(BLEScanInterval); - pBLEScan->setWindow(BLEScanWindow); - NimBLEScanResults foundDevices = pBLEScan->getResults(BTConfig.scanDuration, false); - if (foundDevices.getCount()) - scanCount++; - Log.notice(F("Found %d devices, scan number %d end" CR), foundDevices.getCount(), scanCount); - Log.trace(F("Process BLE stack free: %u" CR), uxTaskGetStackHighWaterMark(xProcBLETaskHandle)); -} - -/** - * Connect to BLE devices and initiate the callbacks with a service/characteristic request - */ -# if BLEDecoder -void BLEconnect() { - if (!BTProcessLock) { - Log.notice(F("BLE Connect begin" CR)); - do { - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - BLEdevice* p = *it; - if (p->connect) { - Log.trace(F("Model to connect found: %s" CR), p->macAdr); - NimBLEAddress addr((const char*)p->macAdr, p->macType); - if (p->sensorModel_id == BLEconectable::id::LYWSD03MMC || - p->sensorModel_id == BLEconectable::id::MHO_C401) { - LYWSD03MMC_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - BLEclient.publishData(); - } else if (p->sensorModel_id == BLEconectable::id::DT24_BLE) { - DT24_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - BLEclient.publishData(); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { - BM2_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); - BLEclient.publishData(); - } else if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { - XMWSDJ04MMC_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - BLEclient.publishData(); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1) { - SBS1_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT) { - SBBT_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU) { - SBCU_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - } else { - GENERIC_connect BLEclient(addr); - if (BLEclient.processActions(BLEactions)) { - // If we don't regularly connect to this, disable connections so advertisements - // won't be filtered if BLE_FILTER_CONNECTABLE is set. - p->connect = false; - } - } - if (BLEactions.size() > 0) { - std::vector swap; - for (auto& it : BLEactions) { - if (!it.complete && --it.ttl) { - swap.push_back(it); - } else if (it.addr == NimBLEAddress(p->macAdr, p->macType)) { - if (p->sensorModel_id != BLEconectable::id::DT24_BLE && - 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 != BLEconectable::id::MHO_C401 && - p->sensorModel_id != BLEconectable::id::XMWSDJ04MMC) { - // if irregulary connected to and connection failed clear the connect flag. - p->connect = false; - } - } - } - std::swap(BLEactions, swap); - } - } - } - } while (BLEactions.size() > 0); - Log.notice(F("BLE Connect end" CR)); - } -} -# else -void BLEconnect() {} -# endif - -void stopProcessing(bool deinit) { - if (BTConfig.enabled) { - BTProcessLock = true; - // We stop the scan - Log.notice(F("Stopping BLE scan" CR)); - BLEScan* pBLEScan = BLEDevice::getScan(); - if (pBLEScan->isScanning()) { - pBLEScan->stop(); - } - - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { - Log.notice(F("Stopping BLE tasks" CR)); - //Suspending, deleting tasks and stopping BT to free memory - vTaskSuspend(xCoreTaskHandle); - vTaskDelete(xCoreTaskHandle); - vTaskSuspend(xProcBLETaskHandle); - vTaskDelete(xProcBLETaskHandle); - xSemaphoreGive(semaphoreBLEOperation); - } - // Using deinit to free memory, should only be used if we are going to restart the gateway - if (deinit) - BLEDevice::deinit(true); - } - Log.notice(F("BLE gateway stopped, free heap: %d" CR), ESP.getFreeHeap()); -} - -void coreTask(void* pvParameters) { - while (true) { - if (!BTProcessLock) { - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(30000)) == pdTRUE) { - BLEscan(); - // Launching a connect every TimeBtwConnect - if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { - timeBetweenConnect = millis(); - BLEconnect(); - } - //dumpDevices(); - Log.trace(F("CoreTask stack free: %u" CR), uxTaskGetStackHighWaterMark(xCoreTaskHandle)); - xSemaphoreGive(semaphoreBLEOperation); - } else { - Log.error(F("Failed to start scan - BLE busy" CR)); - } - if (SYSConfig.powerMode > 0) { - int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); // is this enough, it will wait the full deepsleep... - if (scan == 1) BTforceScan(); - ready_to_sleep = true; - } else { - for (int interval = BTConfig.BLEinterval, waitms; interval > 0; interval -= waitms) { - int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); - if (scan == 1) BTforceScan(); // should we break after this? - delay(waitms = interval > 100 ? 100 : interval); // 100ms - } - } - } - delay(1); - } -} - -void setupBTTasksAndBLE() { -# ifdef CONFIG_BTDM_BLE_SCAN_DUPL - BLEDevice::setScanDuplicateCacheSize(BLEScanDuplicateCacheSize); -# endif - BLEDevice::init(""); - xTaskCreateUniversal( - procBLETask, /* Function to implement the task */ - "procBLETask", /* Name of the task */ -# if defined(USE_ESP_IDF) || defined(USE_BLUFI) - 14500, -# else - 9500, /* Stack size in bytes */ -# endif - NULL, /* Task input parameter */ - 2, /* Priority of the task (set higher than core task) */ - &xProcBLETaskHandle, /* Task handle. */ - 1); /* Core where the task should run */ - - // we setup a task with priority one to avoid conflict with other gateways - xTaskCreateUniversal( - coreTask, /* Function to implement the task */ - "coreTask", /* Name of the task */ - 5120, /* Stack size in bytes */ - NULL, /* Task input parameter */ - 1, /* Priority of the task */ - &xCoreTaskHandle, /* Task handle. */ - taskCore); /* Core where the task should run */ -} - -void setupBT() { - BTConfig_init(); - BTConfig_load(); - Log.notice(F("BLE scans interval: %d" CR), BTConfig.BLEinterval); - Log.notice(F("BLE connects interval: %d" CR), BTConfig.intervalConnect); - Log.notice(F("BLE scan duration: %d" CR), BTConfig.scanDuration); - Log.notice(F("Publishing only BLE sensors: %T" CR), BTConfig.pubOnlySensors); - Log.notice(F("Publishing random MAC devices: %T" CR), BTConfig.pubRandomMACs); - Log.notice(F("Adaptive BLE scan: %T" CR), BTConfig.adaptiveScan); - Log.notice(F("Active BLE scan interval: %d" CR), BTConfig.intervalActiveScan); - Log.notice(F("minrssi: %d" CR), -abs(BTConfig.minRssi)); - Log.notice(F("Presence Away Timer: %d" CR), BTConfig.presenceAwayTimer); - Log.notice(F("Moving Timer: %d" CR), BTConfig.movingTimer); - Log.notice(F("Force passive scan: %T" CR), BTConfig.forcePassiveScan); - Log.notice(F("Enabled BLE: %T" CR), BTConfig.enabled); - - atomic_init(&forceBTScan, 0); // in theory, we don't need this - - semaphoreCreateOrUpdateDevice = xSemaphoreCreateBinary(); - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - - semaphoreBLEOperation = xSemaphoreCreateBinary(); - xSemaphoreGive(semaphoreBLEOperation); - - BLEQueue = xQueueCreate(QueueSize, sizeof(NimBLEAdvertisedDevice*)); - if (BTConfig.enabled) { - setupBTTasksAndBLE(); - Log.notice(F("gatewayBT multicore ESP32 setup done" CR)); - } else { - Log.notice(F("gatewayBT multicore ESP32 setup disabled" CR)); - } -} - -boolean valid_service_data(const char* data, int size) { - for (int i = 0; i < size; ++i) { - if (data[i] != 48) // 48 correspond to 0 in ASCII table - return true; - } - return false; -} - -# if defined(ZmqttDiscovery) && BLEDecoder == true -// This function always should be called from the main core as it generates direct mqtt messages -// When overrideDiscovery=true, we publish discovery messages of known devices (even if no new) -void launchBTDiscovery(bool overrideDiscovery) { - if (!overrideDiscovery && newDevices == 0) - return; - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdFALSE) { - Log.error(F("Semaphore NOT taken" CR)); - return; - } - newDevices = 0; - vector localDevices = devices; - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - for (vector::iterator it = localDevices.begin(); it != localDevices.end(); ++it) { - BLEdevice* p = *it; - Log.trace(F("Device mac %s" CR), p->macAdr); - // Do not launch discovery for the devices already discovered (unless we have overrideDiscovery) or that are not unique by their MAC Address (iBeacon, GAEN and Microsoft CDP) - if (overrideDiscovery || !isDiscovered(p)) { - String macWOdots = String(p->macAdr); - macWOdots.replace(":", ""); - if (p->sensorModel_id >= 0) { - Log.trace(F("Looking for Model_id: %d" CR), p->sensorModel_id); - std::string properties = decoder.getTheengProperties(p->sensorModel_id); - Log.trace(F("properties: %s" CR), properties.c_str()); - std::string brand = decoder.getTheengAttribute(p->sensorModel_id, "brand"); - std::string model = decoder.getTheengAttribute(p->sensorModel_id, "model"); - if (displayDeviceName || ForceDeviceName) { - if (p->name[0] != '\0') { - model = p->name; - } - } - std::string model_id = decoder.getTheengAttribute(p->sensorModel_id, "model_id"); - - // Check for tracker status - bool isTracker = false; - std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); - if (tag.length() >= 4) { - isTracker = checkIfIsTracker(tag[3]); - } - - String discovery_topic = String(subjectBTtoMQTT) + "/" + macWOdots; - 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 - if (isTracker) { - String tracker_name = String(model_id.c_str()) + "-tracker"; - String tracker_id = macWOdots + "-tracker"; - createDiscovery("device_tracker", - discovery_topic.c_str(), tracker_name.c_str(), 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 == TheengsDecoder::BLE_ID_NUM::BC08) { - String sensor_name = String(model_id.c_str()) + "-moving"; - String sensor_id = macWOdots + "-moving"; - createDiscovery("binary_sensor", - discovery_topic.c_str(), sensor_name.c_str(), sensor_id.c_str(), - will_Topic, "moving", "{% if value_json.get('accx') -%}on{%- else -%}off{%- endif %}", - "on", "off", "", - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } - if (displayDeviceName && p->sensorModel_id >= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_ATC && p->sensorModel_id <= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_PVVX_BTHOME_2 ) { - xxWSD0xMMCDiscovery(macWOdots.c_str(), p->name, model_id.c_str()); - } else if (!properties.empty()) { - StaticJsonDocument jsonBuffer; - auto error = deserializeJson(jsonBuffer, properties); - if (error) { - if (jsonBuffer.overflowed()) { - // This should not happen if JSON_MSG_BUFFER is large enough for - // the Theengs json properties - Log.error(F("JSON deserialization of Theengs properties overflowed (error %s), buffer capacity: %u. Program might crash. Properties json: %s" CR), - error.c_str(), jsonBuffer.capacity(), properties.c_str()); - } else { - Log.error(F("JSON deserialization of Theengs properties errored: %" CR), - error.c_str()); - } - } - for (JsonPair prop : jsonBuffer["properties"].as()) { - Log.trace(F("Key: %s"), prop.key().c_str()); - Log.trace(F("Unit: %s"), prop.value()["unit"].as()); - Log.trace(F("Name: %s"), prop.value()["name"].as()); - String entity_name = ""; - if (displayDeviceName || ForceDeviceName) { - entity_name = String(model.c_str()) + "-" + String(prop.key().c_str()); - } else { - entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); - } - String unique_id = macWOdots + "-" + String(prop.key().c_str()); - String value_template = "{{ value_json." + String(prop.key().c_str()) + " | is_defined }}"; - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1 && strcmp(prop.key().c_str(), "state") == 0) { - String payload_on = "{\"model_id\":\"X1\",\"cmd\":\"on\",\"id\":\"" + String(p->macAdr) + "\"}"; - String payload_off = "{\"model_id\":\"X1\",\"cmd\":\"off\",\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("switch", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "switch", value_template.c_str(), - payload_on.c_str(), payload_off.c_str(), "", 0, - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone, "off", "on"); - unique_id = macWOdots + "-press"; - entity_name = String(model_id.c_str()) + "-press"; - String payload_press = "{\"model_id\":\"X1\",\"cmd\":\"press\",\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("button", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "button", "", - payload_press.c_str(), "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT && strcmp(prop.key().c_str(), "open") == 0) { - value_template = "{% if value_json.direction == \"up\" -%} {{ 100 - value_json.open/2 }}{% elif value_json.direction == \"down\" %}{{ value_json.open/2 }}{% else %} {{ value_json.open/2 }}{%- endif %}"; - String command_template = "{\"model_id\":\"W270160X\",\"tilt\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("cover", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "cover", value_template.c_str(), - "50", "", "", 0, - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - "blind", nullptr, nullptr, nullptr, command_template.c_str()); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU && strcmp(prop.key().c_str(), "position") == 0) { - String command_template = "{\"model_id\":\"W070160X\",\"position\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("cover", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "cover", "{{ value_json.position }}", - "0", "100", "", 0, - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - "curtain", nullptr, nullptr, nullptr, command_template.c_str()); - } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && - strcmp(prop.key().c_str(), "weighing_mode") == 0) { - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "enum", value_template.c_str(), - "", "", prop.value()["unit"], - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassMeasurement, nullptr, nullptr, "[\"person\",\"object\"]"); - } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && - strcmp(prop.key().c_str(), "unit") == 0) { - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "enum", value_template.c_str(), - "", "", prop.value()["unit"], - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassMeasurement, nullptr, nullptr, "[\"lb\",\"kg\",\"jin\"]"); - } else if (strcmp(prop.value()["unit"], "string") == 0 && strcmp(prop.key().c_str(), "mac") != 0) { - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "", "", "", - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (p->sensorModel_id == TheengsDecoder::MUE4094RT && strcmp(prop.value()["unit"], "status") == 0) { // This device does not a broadcast when there is nothing detected so adding a timeout - createDiscovery("binary_sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "True", "False", "", - BTConfig.presenceAwayTimer / 1000, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (strcmp(prop.value()["unit"], "status") == 0) { - createDiscovery("binary_sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "True", "False", "", - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (strcmp(prop.key().c_str(), "device") != 0 && strcmp(prop.key().c_str(), "mac") != 0) { // Exception on device and mac as these ones are not sensors - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "", "", prop.value()["unit"], - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassMeasurement); - } - } - } - } 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) { - // 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"); - } - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { - // Sensor discovery - BM2Discovery(macWOdots.c_str(), "BM2"); - // Device tracker discovery - String tracker_id = macWOdots + "-tracker"; - createDiscovery("device_tracker", - discovery_topic.c_str(), "BM2-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"); - } - if (p->sensorModel_id == BLEconectable::id::MHO_C401) { - MHO_C401Discovery(macWOdots.c_str(), "MHO-C401"); - } - if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { - XMWSDJ04MMCDiscovery(macWOdots.c_str(), "XMWSDJ04MMC"); - } - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC) { - HHCCJCY01HHCCDiscovery(macWOdots.c_str(), "HHCCJCY01HHCC"); - } - } else { - Log.trace(F("Device UNKNOWN_MODEL %s" CR), p->macAdr); - } - } - } - p->isDisc = true; // we don't need the semaphore and all the search magic via createOrUpdateDevice - } else { - Log.trace(F("Device already discovered or that doesn't require discovery %s" CR), p->macAdr); - } - } -} -# else -void launchBTDiscovery(bool overrideDiscovery) {} -# endif - -# if BLEDecryptor -// ** TODO - Hex string to bytes, there is probably a function for this already just need to find it -int hexToBytes(String hex, uint8_t *out, size_t maxLen) { - int len = hex.length(); - if (len % 2 || len / 2 > maxLen) return -1; - for (int i = 0, j = 0; i < len; i += 2, j++) { - out[j] = (uint8_t) strtol(hex.substring(i, i + 2).c_str(), nullptr, 16); - } - return len / 2; -} -// Reverse bytes -void reverseBytes(uint8_t *data, size_t length) { - size_t i; - for (i = 0; i < length / 2; i++) { - uint8_t temp = data[i]; - data[i] = data[length - 1 - i]; - data[length - 1 - i] = temp; - } -} -# endif - -# if BLEDecoder -void process_bledata(JsonObject& BLEdata) { - yield(); // Necessary to let the loop run in case of connectivity issues - if (!BLEdata.containsKey("id")) { - Log.error(F("No mac address in the payload" CR)); - return; - } - const char* mac = BLEdata["id"].as(); - Log.trace(F("Processing BLE data %s" CR), BLEdata["id"].as()); - int model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); - int mac_type = BLEdata["mac_type"].as(); - -# if BLEDecryptor - if (BLEdata["encr"] && (BLEdata["encr"].as() >0 && BLEdata["encr"].as() <=2)) { - // Decrypting Encrypted BLE Data PVVX, BTHome or Victron - Log.trace(F("[BLEDecryptor] Decrypt ENCR:%d ModelID:%s Payload:%s" CR), BLEdata["encr"].as(), BLEdata["model_id"].as(), BLEdata["cipher"].as()); - - // MAC address - String macWOdots = BLEdata["id"].as(); // Mac Address without dots - macWOdots.replace(":", ""); - unsigned char macAddress[6]; - int maclen = hexToBytes(macWOdots, macAddress, 6); - if (maclen != 6) { - Log.error(F("[BLEDecryptor] Invalid MAC Address length %d" CR), maclen); - return; - } - - // AES decryption key - unsigned char bleaeskey[16]; - int bleaeskeylength = 0; - if (ble_aes_keys.containsKey(macWOdots)){ - Log.trace(F("[BLEDecryptor] Custom AES key %s" CR), ble_aes_keys[macWOdots].as()); - bleaeskeylength = hexToBytes(ble_aes_keys[macWOdots], bleaeskey, 16); - } else { - Log.trace(F("[BLEDecryptor] Default AES key" CR)); - bleaeskeylength = hexToBytes(ble_aes, bleaeskey, 16); - } - // Check AES Key - if (bleaeskeylength != 16) { - Log.error(F("[BLEDecryptor] Invalid key length %d" CR), bleaeskeylength); - return; - } - - // Build nonce and aad - uint8_t nonce[16]; - int noncelength = 0; - unsigned char aad[1]; - int aadLength; - - if (BLEdata["encr"].as() == 1){ // PVVX Encrypted - noncelength = 11; // 11 bytes - reverseBytes(macAddress, 6); // 6 bytes: device address in reverse - memcpy(nonce, macAddress, 6); - int maclen = hexToBytes(macWOdots, macAddress, 6); - - unsigned char servicedata[16]; - int servicedatalen = hexToBytes(BLEdata["servicedata"].as(), servicedata, 16); - nonce[6] = servicedatalen + 3; // 1 byte : length of (service data + type and UUID) - nonce[7] = 0x16; // 1 byte : "16" -> AD type for "Service Data - 16-bit UUID" - nonce[8] = 0x1A; // 2 bytes: "1a18" -> UUID 181a in little-endian - nonce[9] = 0x18; // - unsigned char ctr[1]; // 1 byte : counter - int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 1); - if (ctrlen != 1) { - Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); - return; - } - nonce[10] = ctr[0]; - aad[0] = 0x11; - aadLength = 1; - Log.trace(F("[BLEDecryptor] PVVX nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); - - } else if (BLEdata["encr"].as() == 2){ // BTHome V2 Encrypted - noncelength = 13; // 13 bytes - memcpy(nonce, macAddress, 6); - nonce[6] = 0xD2; // UUID - nonce[7] = 0xFC; - nonce[8] = 0x41; // BTHome Device Data encrypted payload byte - unsigned char ctr[4]; // Counter - int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 4); - if (ctrlen != 4) { - Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); - return; - } - memcpy(&nonce[9], ctr, 4); - aad[0] = 0x00; - aadLength = 0; - Log.trace(F("[BLEDecryptor] BTHomeV2 nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); - - } else if (BLEdata["encr"].as() == 3){ - nonce[16] = {0}; // Victron has a 16 byte zero padded nonce with IV bytes 6,7 - unsigned char iv[2]; - int ivlen = hexToBytes(BLEdata["ctr"].as(), iv, 2); - if (ivlen != 2) { - Log.error(F("[BLEDecryptor] Invalid iv length %d" CR), ivlen); - return; - } - memcpy(nonce, iv, 2); - Log.trace(F("[BLEDecryptor] Victron nonce %s" CR), NimBLEUtils::dataToHexString(nonce, 16).c_str()); - } else { - return; // No match - } - - // Ciphertext to bytes - int cipherlen = sizeof(BLEdata["cipher"].as()); - unsigned char ciphertext[cipherlen]; - int ciphertextlen = hexToBytes(BLEdata["cipher"].as(), ciphertext, cipherlen); - unsigned char decrypted[ciphertextlen]; // Decrypted payload - - // Decrypt ciphertext - if (BLEdata["encr"].as() == 1 || BLEdata["encr"].as() == 2) { - // Decrypt PVVX and BTHome V2 ciphertext using AES CCM - mbedtls_ccm_context ctx; - mbedtls_ccm_init(&ctx); - if (mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, bleaeskey, 128) != 0) { - Log.error(F("[BLEDecryptor] Failed to set AES key to mbedtls" CR)); - return; - } - - // Message Integrity Check (MIC) - unsigned char mic[4]; - int miclen = hexToBytes(BLEdata["mic"].as(), mic, 4); - if (miclen != 4) { - Log.error(F("[BLEDecryptor] Invalid MIC length %d" CR), miclen); - return; - } - - int ret = mbedtls_ccm_auth_decrypt( - &ctx, // AES Key - ciphertextlen, // length of ciphertext - nonce, noncelength, // Nonce - aad, aadLength, // AAD - ciphertext, // input ciphertext - decrypted, // output plaintext - mic, sizeof(mic) // Message Integrity Check - ); - mbedtls_ccm_free(&ctx); - - if (ret == 0) { - Log.notice(F("[BLEDecryptor] Decryption successful" CR)); - } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { - Log.error(F("[BLEDecryptor] Authentication failed." CR)); - return; - } else { - Log.error(F("[BLEDecryptor] Decryption failed with error: %X" CR), ret); - return; - } - - // Build new servicedata - if (BLEdata["encr"].as() == 1){ // PVVX - BLEdata["servicedata"] = NimBLEUtils::dataToHexString(decrypted, ciphertextlen); - } else if (BLEdata["encr"].as() == 2) { // BTHomeV2 - // Build new servicedata - uint8_t newservicedata[3 + ciphertextlen]; - newservicedata[0] = 0x40; // Decrypted BTHomeV2 Packet Type - newservicedata[1] = 0x00; // Packet counter which the PVVX BTHome non-encrypted has but the encrypted does not - newservicedata[2] = 0x00; // **TODO Convert the ctr to the packet counter or just stick with 0? - memcpy(&newservicedata[3], decrypted, ciphertextlen); - BLEdata["servicedata"] = NimBLEUtils::dataToHexString(newservicedata, ciphertextlen + 3); - } else { - return; - } - Log.trace(F("[BLEDecryptor] Decrypted servicedata %s" CR), BLEdata["servicedata"].as()); - - } else if (BLEdata["encr"].as() == 3) { - // Decrypt Victron Energy encrypted advertisements. - size_t nc_off = 0; - uint8_t stream_block[16] = {0}; - - mbedtls_aes_context ctx; - mbedtls_aes_init(&ctx); - mbedtls_aes_setkey_enc(&ctx, bleaeskey, 128); - int ret = mbedtls_aes_crypt_ctr( - &ctx, // AES Key - ciphertextlen, // length of ciphertext - &nc_off, - nonce, // 16 byte nonce with 2 bytes iv - stream_block, - ciphertext, // input ciphertext - decrypted // output plaintext - ); - mbedtls_aes_free(&ctx); - - if (ret == 0) { - Log.notice(F("[BLEDecryptor] Victron Decryption successful" CR)); - } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { - Log.error(F("[BLEDecryptor] Victron Authentication failed." CR)); - return; - } else { - Log.error(F("[BLEDecryptor] Victron decryption failed with error: %X" CR), ret); - return; - } - - // Build new manufacturerdata - unsigned char manufacturerdata[10 + ciphertextlen]; - int manufacturerdatalen = hexToBytes(BLEdata["manufacturerdata"].as(), manufacturerdata, 10); - manufacturerdata[2] = 0x11; // Replace byte 2 with "11" indicate decrypted data - manufacturerdata[7] = 0xff; // Replace byte 7 with "ff" to indicate decrypted data - manufacturerdata[8] = 0xff; // Replace byte 8 with "ff" to indicate decrypted data - memcpy(&manufacturerdata[8], decrypted, ciphertextlen); // Append the decrypted payload to the manufacturer data - BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString(manufacturerdata, 10 + ciphertextlen); // Rebuild manufacturerdata - Log.trace(F("[BLEDecryptor] Victron decrypted manufacturerdata %s" CR), BLEdata["manufacturerdata"].as()); - } - - // Print before and after decoder post decryption - // serializeJsonPretty(BLEdata, Serial); - model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); - // serializeJsonPretty(BLEdata, Serial); - Log.trace(F("[BLEDecryptor] Decrypted model_id %d" CR), model_id); - - // Remove the cipher fields from BLEdata - BLEdata.remove("encr"); - BLEdata.remove("cipher"); - BLEdata.remove("ctr"); - BLEdata.remove("mic"); - - } -# endif - - // Convert prmacs to RMACS until or if OMG gets Identity MAC/IRK decoding - if (BLEdata["prmac"]) { - BLEdata.remove("prmac"); - if (BLEdata["track"]) { - BLEdata.remove("track"); - } - BLEdata["type"] = "RMAC"; - Log.trace(F("Potential RMAC (prmac) converted to RMAC" CR)); - } - const char* deviceName = BLEdata["name"] | ""; - - 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 - 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 - createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); - } else { - createOrUpdateDevice(mac, device_flags_init, model_id, mac_type, deviceName); - if (BTConfig.adaptiveScan == true && (BTConfig.BLEinterval != MinTimeBtwScan || BTConfig.intervalActiveScan != MinTimeBtwScan)) { - if (BLEdata.containsKey("acts") && BLEdata.containsKey("cont")) { - if (BLEdata["acts"] && BLEdata["cont"]) { - BTConfig.BLEinterval = MinTimeBtwScan; - BTConfig.intervalActiveScan = MinTimeBtwScan; - BTConfig.scanDuration = MinScanDuration; - Log.notice(F("Active and continuous scanning required, parameters adapted" CR)); - // stateBTMeasures(false); - } - } else if (BLEdata.containsKey("cont") && BTConfig.BLEinterval != MinTimeBtwScan) { - if (BLEdata["cont"]) { - BTConfig.BLEinterval = MinTimeBtwScan; - if ((BLEdata["type"].as()).compare("CTMO") == 0) { - BTConfig.scanDuration = MinScanDuration; - } - Log.notice(F("Passive continuous scanning required, parameters adapted" CR)); - // stateBTMeasures(false); - } - } - } - } - } else { - if (BLEdata.containsKey("name")) { // Connectable only devices - std::string name = BLEdata["name"]; - if (name.compare("LYWSD03MMC") == 0) - model_id = BLEconectable::id::LYWSD03MMC; - else if (name.compare("DT24-BLE") == 0) - model_id = BLEconectable::id::DT24_BLE; - else if (name.compare("MHO-C401") == 0) - model_id = BLEconectable::id::MHO_C401; - else if (name.compare("XMWSDJ04MMC") == 0) - model_id = BLEconectable::id::XMWSDJ04MMC; - - if (model_id > 0) { - Log.trace(F("Connectable device found: %s" CR), name.c_str()); - createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); - } - } else if (BTConfig.extDecoderEnable && model_id < 0 && BLEdata.containsKey("servicedata")) { - const char* service_data = (const char*)(BLEdata["servicedata"] | ""); - if (strstr(service_data, "209800") != NULL) { - model_id = TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC; - Log.trace(F("Connectable device found: HHCCJCY01HHCC" CR)); - createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); - } - } - } - } else { - Log.trace(F("Random MAC or iBeacon device filtered" CR)); - } - if (!BTConfig.extDecoderEnable && model_id < 0) { - Log.trace(F("No eligible device found " CR)); - } -} -void PublishDeviceData(JsonObject& BLEdata) { - if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough - // Decode the payload - process_bledata(BLEdata); - // If the device is a random MAC and pubRandomMACs is false we don't publish this payload - if (!BTConfig.pubRandomMACs && (BLEdata["type"].as()).compare("RMAC") == 0) { - Log.trace(F("Random MAC, device filtered" CR)); - return; - } - // If pubAdvData is false we don't publish the adv data - if (!BTConfig.pubAdvData) { - BLEdata.remove("servicedatauuid"); - BLEdata.remove("servicedata"); - BLEdata.remove("manufacturerdata"); - BLEdata.remove("mac_type"); - BLEdata.remove("adv_type"); - // tag device properties - // BLEdata.remove("type"); type is used by the WebUI module to determine the template used to display the signal - BLEdata.remove("cidc"); - BLEdata.remove("acts"); - BLEdata.remove("cont"); - BLEdata.remove("track"); - BLEdata.remove("ctrl"); - } - // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid - if (BLEdata.containsKey("distance")) { - if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { - BLEdata["mac"] = BLEdata["id"].as(); - BLEdata["id"] = BLEdata["uuid"].as(); - } - String topic = String(mqtt_topic) + BTConfig.presenceTopic + String(gateway_name); - Log.trace(F("Pub HA Presence %s" CR), topic.c_str()); - BLEdata["topic"] = topic; - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } - - // If the device is not a sensor and pubOnlySensors is true we don't publish this payload - if (!BTConfig.pubOnlySensors || BLEdata.containsKey("model") || !BLEDecoder) { // Identified device - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } else { - Log.notice(F("Not a sensor device filtered" CR)); - return; - } - -# if BLEDecoder - if (enableMultiGTWSync && BLEdata.containsKey("model_id") && BLEdata.containsKey("id")) { - // Publish tracker sync message - bool isTracker = false; - std::string tag = decoder.getTheengAttribute(BLEdata["model_id"].as(), "tag"); - if (tag.length() >= 4) { - isTracker = checkIfIsTracker(tag[3]); - } - - if (isTracker) { - StaticJsonDocument BLEdataBuffer; - JsonObject TrackerSyncdata = BLEdataBuffer.to(); - TrackerSyncdata["gatewayid"] = gateway_name; - TrackerSyncdata["trackerid"] = BLEdata["id"].as(); - String topic = String(mqtt_topic) + String(subjectTrackerSync); - TrackerSyncdata["topic"] = topic.c_str(); - enqueueJsonObject(TrackerSyncdata); - } - } -# endif - } else { - Log.notice(F("Low rssi, device filtered" CR)); - return; - } -} -# else -void process_bledata(JsonObject& BLEdata) {} -void PublishDeviceData(JsonObject& BLEdata) { - if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough - // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid - if (BLEdata.containsKey("distance")) { - if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { - BLEdata["mac"] = BLEdata["id"].as(); - BLEdata["id"] = BLEdata["uuid"].as(); - } - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } else { - Log.notice(F("Low rssi, device filtered" CR)); - return; - } -} -# endif - -void hass_presence(JsonObject& HomePresence) { - int BLErssi = HomePresence["rssi"]; - Log.trace(F("BLErssi %d" CR), BLErssi); - int txPower = HomePresence["txpower"] | 0; - if (txPower >= 0) - txPower = -59; //if tx power is not found we set a default calibration value - Log.trace(F("TxPower: %d" CR), txPower); - double ratio = BLErssi * 1.0 / txPower; - double distance; - if (ratio < 1.0) { - distance = pow(ratio, 10); - } else { - distance = (0.89976) * pow(ratio, 7.7095) + 0.111; - } - HomePresence["distance"] = distance; - Log.trace(F("Ble distance %D" CR), distance); -} - -void BTforceScan() { - if (!BTProcessLock) { - BLEscan(); - Log.trace(F("Scan done" CR)); - if (BTConfig.bleConnect) - BLEconnect(); - } else { - Log.trace(F("Cannot launch scan due to other process running" CR)); - } -} - -void immediateBTAction(void* pvParameters) { - if (BLEactions.size()) { - // Immediate action; we need to prevent the normal connection action and stop scanning - BTProcessLock = true; - NimBLEScan* pScan = NimBLEDevice::getScan(); - if (pScan->isScanning()) { - pScan->stop(); - } - - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { - // swap the vectors so only this device is processed - std::vector dev_swap; - dev_swap.push_back(getDeviceByMac(BLEactions.back().addr.toString().c_str())); - std::swap(devices, dev_swap); - - std::vector act_swap; - act_swap.push_back(BLEactions.back()); - BLEactions.pop_back(); - std::swap(BLEactions, act_swap); - - // Unlock here to allow the action to be performed - BTProcessLock = false; - BLEconnect(); - // back to normal - std::swap(devices, dev_swap); - std::swap(BLEactions, act_swap); - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - } else { - Log.error(F("CreateOrUpdate Semaphore NOT taken" CR)); - } - - // If we stopped the scheduled connect for this action, do the scheduled now - if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { - timeBetweenConnect = millis(); - BLEconnect(); - } - xSemaphoreGive(semaphoreBLEOperation); - } else { - Log.error(F("BLE busy - immediateBTAction not sent" CR)); - gatewayState = GatewayState::ERROR; - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = BLEactions.back().addr.toString(); - BLEdata["success"] = false; - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - BLEactions.pop_back(); - BTProcessLock = false; - } - } - vTaskDelete(NULL); -} - -void startBTActionTask() { - TaskHandle_t th; - xTaskCreateUniversal( - immediateBTAction, /* Function to implement the task */ - "imActTask", /* Name of the task */ - 8000, /* Stack size in bytes */ - NULL, /* Task input parameter */ - 3, /* Priority of the task (set higher than core task) */ - &th, /* Task handle. */ - 1); /* Core where the task should run */ -} - -# if BLEDecoder -void KnownBTActions(JsonObject& BTdata) { - if (!BTdata.containsKey("id")) { - Log.error(F("BLE mac address missing" CR)); - gatewayState = GatewayState::ERROR; - return; - } - - BLEAction action{}; - action.write = true; - action.ttl = 3; - bool res = false; - if (BTdata.containsKey("model_id") && BTdata["model_id"].is()) { - if (BTdata["model_id"] == "X1") { - if (BTdata.containsKey("cmd") && BTdata["cmd"].is()) { - action.value_type = BLE_VAL_STRING; - std::string val = BTdata["cmd"].as(); // Fix #1694 - action.value = val; - createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, - TheengsDecoder::BLE_ID_NUM::SBS1, 1); - res = true; - } - } else if (BTdata["model_id"] == "W270160X") { - if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { - action.value_type = BLE_VAL_INT; - res = true; - } else if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { - action.value_type = BLE_VAL_STRING; - res = true; - } - if (res) { - std::string val = BTdata["tilt"].as(); // Fix #1694 - action.value = val; - createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, - TheengsDecoder::BLE_ID_NUM::SBBT, 1); - } - } else if (BTdata["model_id"] == "W070160X") { - if (BTdata.containsKey("position") && BTdata["position"].is()) { - action.value_type = BLE_VAL_INT; - res = true; - } else if (BTdata.containsKey("position") && BTdata["position"].is()) { - action.value_type = BLE_VAL_STRING; - res = true; - } - if (res) { - std::string val = BTdata["position"].as(); // Fix #1694 - action.value = val; - createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, - TheengsDecoder::BLE_ID_NUM::SBCU, 1); - } - } - if (res) { - action.addr = NimBLEAddress(BTdata["id"].as(), 1); - BLEactions.push_back(action); - startBTActionTask(); - } else { - Log.error(F("BLE action not recognized" CR)); - gatewayState = GatewayState::ERROR; - } - } -} -# else -void KnownBTActions(JsonObject& BTdata) {} -# endif - -void XtoBTAction(JsonObject& BTdata) { - BLEAction action{}; - action.ttl = BTdata.containsKey("ttl") ? (uint8_t)BTdata["ttl"] : 1; - action.value_type = BLE_VAL_STRING; - if (BTdata.containsKey("value_type")) { - String vt = BTdata["value_type"]; - vt.toUpperCase(); - if (vt == "HEX") - action.value_type = BLE_VAL_HEX; - else if (vt == "INT") - action.value_type = BLE_VAL_INT; - else if (vt == "FLOAT") - action.value_type = BLE_VAL_FLOAT; - else if (vt != "STRING") { - Log.error(F("BLE value type invalid %s" CR), vt.c_str()); - return; - } - } - - Log.trace(F("BLE ACTION TTL = %u" CR), action.ttl); - action.complete = false; - if (BTdata.containsKey("ble_write_address") && - BTdata.containsKey("ble_write_service") && - BTdata.containsKey("ble_write_char") && - BTdata.containsKey("ble_write_value")) { - action.addr = NimBLEAddress(BTdata["ble_write_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); - action.service = NimBLEUUID((const char*)BTdata["ble_write_service"]); - action.characteristic = NimBLEUUID((const char*)BTdata["ble_write_char"]); - std::string val = BTdata["ble_write_value"].as(); // Fix #1694 - action.value = val; - action.write = true; - Log.trace(F("BLE ACTION Write" CR)); - } else if (BTdata.containsKey("ble_read_address") && - BTdata.containsKey("ble_read_service") && - BTdata.containsKey("ble_read_char")) { - action.addr = NimBLEAddress(BTdata["ble_read_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); - action.service = NimBLEUUID((const char*)BTdata["ble_read_service"]); - action.characteristic = NimBLEUUID((const char*)BTdata["ble_read_char"]); - action.write = false; - Log.trace(F("BLE ACTION Read" CR)); - } else { - return; - } - - createOrUpdateDevice(action.addr.toString().c_str(), device_flags_connect, UNKWNON_MODEL, action.addr.getType()); - - BLEactions.push_back(action); - if (BTdata.containsKey("immediate") && BTdata["immediate"].as()) { - startBTActionTask(); - } -} - -void XtoBT(const char* topicOri, JsonObject& BTdata) { // json object decoding - if (cmpToMainTopic(topicOri, subjectMQTTtoBTset)) { - Log.trace(F("MQTTtoBT json set" CR)); - - // Black list & white list set - bool WorBupdated; - WorBupdated = updateWorB(BTdata, true); - WorBupdated |= updateWorB(BTdata, false); - - if (WorBupdated) { - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { - //dumpDevices(); - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - } - } - - // Force scan now - if (BTdata.containsKey("interval") && BTdata["interval"] == 0) { - Log.notice(F("BLE forced scan" CR)); - atomic_store_explicit(&forceBTScan, 1, ::memory_order_seq_cst); // ask the other core to do the scan for us - } - - /* - * Configuration modifications priorities: - * First `init=true` and `load=true` commands are executed (if both are present, INIT prevails on LOAD) - * Then parameters included in json are taken in account - * Finally `erase=true` and `save=true` commands are executed (if both are present, ERASE prevails on SAVE) - */ - if (BTdata.containsKey("init") && BTdata["init"].as()) { - // Restore the default (initial) configuration - BTConfig_init(); - } else if (BTdata.containsKey("load") && BTdata["load"].as()) { - // Load the saved configuration, if not initialised - BTConfig_load(); - } - - // Load config from json if available - BTConfig_fromJson(BTdata); - - } else if (cmpToMainTopic(topicOri, subjectMQTTtoBT)) { - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { - KnownBTActions(BTdata); - XtoBTAction(BTdata); - xSemaphoreGive(semaphoreBLEOperation); - } else { - Log.error(F("BLE busy - BTActions not sent" CR)); - gatewayState = GatewayState::ERROR; - } - } else if (strstr(topicOri, subjectTrackerSync) != NULL) { - if (BTdata.containsKey("gatewayid") && BTdata.containsKey("trackerid") && BTdata["gatewayid"] != gateway_name) { - BLEdevice* device = getDeviceByMac(BTdata["trackerid"].as()); - if (device != &NO_BT_DEVICE_FOUND && device->lastUpdate != 0) { - device->lastUpdate = 0; - Log.notice(F("Tracker %s disassociated by gateway %s" CR), BTdata["trackerid"].as(), BTdata["gatewayid"].as()); - } - } - } -} -#endif +/* + OpenMQTTGateway - ESP8266 or Arduino program for home automation + + Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker + Send and receiving command by MQTT + + This gateway enables to: + - publish MQTT data to a topic related to BLE devices data + + Copyright: (c)Florian ROBERT + + This file is part of OpenMQTTGateway. + + OpenMQTTGateway is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenMQTTGateway is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Thanks to wolass https://github.com/wolass for suggesting me HM 10 and dinosd https://github.com/dinosd/BLE_PROXIMITY for inspiring me how to implement the gateway +*/ +#include "User_config.h" + +#ifdef ZgatewayBT +# include "TheengsCommon.h" + +SemaphoreHandle_t semaphoreCreateOrUpdateDevice; +SemaphoreHandle_t semaphoreBLEOperation; +QueueHandle_t BLEQueue; +unsigned long scanCount = 0; +# include +# include +# include +# include +# include +# include + +# include + +# include "TheengsCommon.h" +# include "config_mqttDiscovery.h" +# include "gatewayBLEConnect.h" +# include "soc/timer_group_reg.h" +# include "soc/timer_group_struct.h" + +using namespace std; + +// Global struct to store live BT configuration data +BTConfig_s BTConfig; + +# if BLEDecoder +# include +# if BLEDecryptor +# include "mbedtls/ccm.h" +# include "mbedtls/aes.h" +# endif +TheengsDecoder decoder; +# endif + +static TaskHandle_t xCoreTaskHandle; +static TaskHandle_t xProcBLETaskHandle; + +struct decompose { + int start; + int len; + bool reverse; +}; + +vector BLEactions; + +vector devices; +int newDevices = 0; + +static BLEdevice NO_BT_DEVICE_FOUND = { + {0}, + 0, + false, + false, + false, + false, + (char)UNKWNON_MODEL, + 0, +}; +static bool oneWhite = false; + +extern bool BTProcessLock; +extern int queueLength; + +void setupBTTasksAndBLE(); +bool checkIfIsTracker(char ch); +void hass_presence(JsonObject& HomePresence); +void BTforceScan(); + +void BTConfig_init() { + BTConfig.bleConnect = AttemptBLEConnect; + BTConfig.BLEinterval = TimeBtwRead; + BTConfig.adaptiveScan = AdaptiveBLEScan; + BTConfig.intervalActiveScan = TimeBtwActive; + BTConfig.intervalConnect = TimeBtwConnect; + BTConfig.scanDuration = Scan_duration; + BTConfig.pubOnlySensors = PublishOnlySensors; + BTConfig.pubRandomMACs = PublishRandomMACs; + BTConfig.presenceEnable = HassPresence; + BTConfig.presenceTopic = subjectHomePresence; + BTConfig.presenceUseBeaconUuid = useBeaconUuidForPresence; + BTConfig.minRssi = MinimumRSSI; + BTConfig.extDecoderEnable = UseExtDecoder; + BTConfig.extDecoderTopic = MQTTDecodeTopic; + BTConfig.filterConnectable = BLE_FILTER_CONNECTABLE; + BTConfig.pubAdvData = pubBLEAdvData; + BTConfig.pubBeaconUuidForTopic = useBeaconUuidForTopic; + BTConfig.ignoreWBlist = false; + BTConfig.presenceAwayTimer = PresenceAwayTimer; + BTConfig.movingTimer = MovingTimer; + BTConfig.forcePassiveScan = false; + BTConfig.enabled = EnableBT; +} + +unsigned long timeBetweenConnect = 0; +unsigned long timeBetweenActive = 0; + +String stateBTMeasures(bool start) { + StaticJsonDocument jsonBuffer; + JsonObject jo = jsonBuffer.to(); + jo["bleconnect"] = BTConfig.bleConnect; + jo["interval"] = BTConfig.BLEinterval; + jo["adaptivescan"] = BTConfig.adaptiveScan; + jo["intervalacts"] = BTConfig.intervalActiveScan; + jo["intervalcnct"] = BTConfig.intervalConnect; + jo["scanduration"] = BTConfig.scanDuration; + jo["hasspresence"] = BTConfig.presenceEnable; + jo["prestopic"] = BTConfig.presenceTopic; + jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; + jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value + jo["extDecoderEnable"] = BTConfig.extDecoderEnable; + jo["extDecoderTopic"] = BTConfig.extDecoderTopic; + jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; + jo["ignoreWBlist"] = BTConfig.ignoreWBlist; + jo["forcepscn"] = BTConfig.forcePassiveScan; + jo["tskstck"] = uxTaskGetStackHighWaterMark(xProcBLETaskHandle); + jo["crstck"] = uxTaskGetStackHighWaterMark(xCoreTaskHandle); + jo["enabled"] = BTConfig.enabled; + jo["scnct"] = scanCount; +# if BLEDecoder + jo["onlysensors"] = BTConfig.pubOnlySensors; + jo["randommacs"] = BTConfig.pubRandomMACs; + jo["filterConnectable"] = BTConfig.filterConnectable; + jo["pubadvdata"] = BTConfig.pubAdvData; + jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; + jo["movingtimer"] = BTConfig.movingTimer; +# endif + + if (start) { + Log.notice(F("BT sys: ")); + serializeJsonPretty(jsonBuffer, Serial); + Serial.println(); + return ""; // Do not try to erase/write/send config at startup + } + String output; + serializeJson(jo, output); + jo["origin"] = subjectBTtoMQTT; + enqueueJsonObject(jo, QueueSemaphoreTimeOutTask); + return (output); +} + +void BTConfig_fromJson(JsonObject& BTdata, bool startup = false) { + // Attempts to connect to eligible devices or not + Config_update(BTdata, "bleconnect", BTConfig.bleConnect); + // Identify AdaptiveScan deactivation to pass to continuous mode or activation to come back to default settings + if (startup == false) { + if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == false && BTConfig.presenceEnable == true) { + BTdata["adaptivescan"] = true; + } else if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == true && BTConfig.presenceEnable == false) { + BTdata["adaptivescan"] = false; + } + + if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == false && BTConfig.adaptiveScan == true) { + BTdata["interval"] = MinTimeBtwScan; + BTdata["intervalacts"] = MinTimeBtwScan; + BTdata["scanduration"] = MinScanDuration; + } else if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == true && BTConfig.adaptiveScan == false) { + BTdata["interval"] = TimeBtwRead; + BTdata["intervalacts"] = TimeBtwActive; + BTdata["scanduration"] = Scan_duration; + } + // Identify if the gateway is enabled or not and stop start accordingly + if (BTdata.containsKey("enabled") && BTdata["enabled"] == false && BTConfig.enabled == true) { + // Stop the gateway but without deinit to enable a future BT restart + stopProcessing(false); + } else if (BTdata.containsKey("enabled") && BTdata["enabled"] == true && BTConfig.enabled == false) { + BTProcessLock = false; + setupBTTasksAndBLE(); + } + } + // Home Assistant presence message + Config_update(BTdata, "hasspresence", BTConfig.presenceEnable); + // Time before before active scan + // Scan interval set - and avoid intervalacts to be lower than interval + if (BTdata.containsKey("interval") && BTdata["interval"] != 0) { + BTConfig.adaptiveScan = false; + Config_update(BTdata, "interval", BTConfig.BLEinterval); + if (BTConfig.intervalActiveScan < BTConfig.BLEinterval) { + Config_update(BTdata, "interval", BTConfig.intervalActiveScan); + } + } + // Define if the scan is adaptive or not - and avoid intervalacts to be lower than interval + if (BTdata.containsKey("intervalacts") && BTdata["intervalacts"] < BTConfig.BLEinterval) { + BTConfig.adaptiveScan = false; + // Config_update(BTdata, "interval", BTConfig.intervalActiveScan); + BTConfig.intervalActiveScan = BTConfig.BLEinterval; + } else { + Config_update(BTdata, "intervalacts", BTConfig.intervalActiveScan); + } + // Adaptive scan set + Config_update(BTdata, "adaptivescan", BTConfig.adaptiveScan); + // Time before a connect set + Config_update(BTdata, "intervalcnct", BTConfig.intervalConnect); + // publish all BLE devices discovered or only the identified sensors (like temperature sensors) + Config_update(BTdata, "scanduration", BTConfig.scanDuration); + // define the duration for a scan; in milliseconds + Config_update(BTdata, "onlysensors", BTConfig.pubOnlySensors); + // publish devices which randomly change their MAC addresses + Config_update(BTdata, "randommacs", BTConfig.pubRandomMACs); + // Home Assistant presence message topic + Config_update(BTdata, "prestopic", BTConfig.presenceTopic); + // Home Assistant presence message use iBeacon UUID + Config_update(BTdata, "presuseuuid", BTConfig.presenceUseBeaconUuid); + // Timer to trigger a device state as offline if not seen + Config_update(BTdata, "presenceawaytimer", BTConfig.presenceAwayTimer); + // Timer to trigger a device state as offline if not seen + Config_update(BTdata, "movingtimer", BTConfig.movingTimer); + // Force passive scan + Config_update(BTdata, "forcepscn", BTConfig.forcePassiveScan); + // MinRSSI set + Config_update(BTdata, "minrssi", BTConfig.minRssi); + // Send undecoded device data + Config_update(BTdata, "extDecoderEnable", BTConfig.extDecoderEnable); + // Topic to send undecoded device data + Config_update(BTdata, "extDecoderTopic", BTConfig.extDecoderTopic); + // Sets whether to filter publishing + Config_update(BTdata, "filterConnectable", BTConfig.filterConnectable); + // Publish advertisement data + Config_update(BTdata, "pubadvdata", BTConfig.pubAdvData); + // Use iBeacon UUID as topic, instead of sender (random) MAC address + Config_update(BTdata, "pubuuid4topic", BTConfig.pubBeaconUuidForTopic); + // Disable Whitelist & Blacklist + Config_update(BTdata, "ignoreWBlist", (BTConfig.ignoreWBlist)); + // Enable or disable the BT gateway + Config_update(BTdata, "enabled", BTConfig.enabled); + + stateBTMeasures(startup); + + if (BTdata.containsKey("erase") && BTdata["erase"].as()) { + // Erase config from NVS (non-volatile storage) + preferences.begin(Gateway_Short_Name, false); + if (preferences.isKey("BTConfig")) { + int result = preferences.remove("BTConfig"); + Log.notice(F("BT config erase result: %d" CR), result); + preferences.end(); + return; // Erase prevails on save, so skipping save + } else { + preferences.end(); + Log.notice(F("BT config not found" CR)); + } + } + + if (BTdata.containsKey("save") && BTdata["save"].as()) { + StaticJsonDocument jsonBuffer; + JsonObject jo = jsonBuffer.to(); + jo["bleconnect"] = BTConfig.bleConnect; + jo["interval"] = BTConfig.BLEinterval; + jo["adaptivescan"] = BTConfig.adaptiveScan; + jo["intervalacts"] = BTConfig.intervalActiveScan; + jo["intervalcnct"] = BTConfig.intervalConnect; + jo["scanduration"] = BTConfig.scanDuration; + jo["onlysensors"] = BTConfig.pubOnlySensors; + jo["randommacs"] = BTConfig.pubRandomMACs; + jo["hasspresence"] = BTConfig.presenceEnable; + jo["prestopic"] = BTConfig.presenceTopic; + jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; + jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value + jo["extDecoderEnable"] = BTConfig.extDecoderEnable; + jo["extDecoderTopic"] = BTConfig.extDecoderTopic; + jo["filterConnectable"] = BTConfig.filterConnectable; + jo["pubadvdata"] = BTConfig.pubAdvData; + jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; + jo["ignoreWBlist"] = BTConfig.ignoreWBlist; + jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; + jo["movingtimer"] = BTConfig.movingTimer; + jo["forcepscn"] = BTConfig.forcePassiveScan; + jo["enabled"] = BTConfig.enabled; + // Save config into NVS (non-volatile storage) + String conf = ""; + serializeJson(jsonBuffer, conf); + preferences.begin(Gateway_Short_Name, false); + int result = preferences.putString("BTConfig", conf); + preferences.end(); + Log.notice(F("BT config save: %s, result: %d" CR), conf.c_str(), result); + } +} + +void BTConfig_load() { + StaticJsonDocument jsonBuffer; + preferences.begin(Gateway_Short_Name, true); + if (preferences.isKey("BTConfig")) { + auto error = deserializeJson(jsonBuffer, preferences.getString("BTConfig", "{}")); + preferences.end(); + Log.notice(F("BT config loaded" CR)); + if (error) { + Log.error(F("BT config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity()); + return; + } + if (jsonBuffer.isNull()) { + Log.warning(F("BT config is null" CR)); + return; + } + JsonObject jo = jsonBuffer.as(); + BTConfig_fromJson(jo, true); // Never send MQTT message with config + Log.notice(F("BT config loaded" CR)); + } else { + preferences.end(); + Log.notice(F("BT config not found" CR)); + } +} + +void PublishDeviceData(JsonObject& BLEdata); + +atomic_int forceBTScan; + +void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type = 0, const char* name = ""); + +BLEdevice* getDeviceByMac(const char* mac); // Declared here to avoid pre-compilation issue (misplaced auto declaration by pio) +BLEdevice* getDeviceByMac(const char* mac) { + Log.trace(F("getDeviceByMac %s" CR), mac); + + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + if ((strcmp((*it)->macAdr, mac) == 0)) { + return *it; + } + } + return &NO_BT_DEVICE_FOUND; +} + +bool updateWorB(JsonObject& BTdata, bool isWhite) { + Log.trace(F("update WorB" CR)); + const char* jsonKey = isWhite ? "white-list" : "black-list"; + + int size = BTdata[jsonKey].size(); + if (size == 0) + return false; + + for (int i = 0; i < size; i++) { + const char* mac = BTdata[jsonKey][i]; + createOrUpdateDevice(mac, (isWhite ? device_flags_isWhiteL : device_flags_isBlackL), + UNKWNON_MODEL); + } + + return true; +} + +void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type, const char* name) { + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(30000)) == pdFALSE) { + Log.error(F("Semaphore NOT taken" CR)); + return; + } + BLEdevice* device = getDeviceByMac(mac); + if (device == &NO_BT_DEVICE_FOUND) { + Log.trace(F("add %s" CR), mac); + //new device + device = new BLEdevice(); + strcpy(device->macAdr, mac); + device->isDisc = flags & device_flags_isDisc; + device->isWhtL = flags & device_flags_isWhiteL; + device->isBlkL = flags & device_flags_isBlackL; + device->connect = flags & device_flags_connect; + device->macType = mac_type; + // Check name length + if (strlen(name) > 20) { + Log.warning(F("Name too long, truncating" CR)); + strncpy(device->name, name, 20); + device->name[19] = '\0'; + } else { + strcpy(device->name, name); + } + device->sensorModel_id = model; + device->lastUpdate = millis(); + devices.push_back(device); + newDevices++; + } else { + Log.trace(F("update %s" CR), mac); + device->lastUpdate = millis(); + device->macType = mac_type; + + if (flags & device_flags_isDisc) { + device->isDisc = true; + } + + if (flags & device_flags_connect) { + device->connect = true; + } + + if (model != UNKWNON_MODEL && device->sensorModel_id == UNKWNON_MODEL) { + newDevices++; + device->isDisc = false; + device->sensorModel_id = model; + } + + // If a device has been added to the white-list, flag it so it can be auto-detected + if (!device->isWhtL && flags & device_flags_isWhiteL) { + newDevices++; + } + if (flags & device_flags_isWhiteL || flags & device_flags_isBlackL) { + device->isWhtL = flags & device_flags_isWhiteL; + device->isBlkL = flags & device_flags_isBlackL; + } + } + + // update oneWhite flag + oneWhite = oneWhite || device->isWhtL; + + xSemaphoreGive(semaphoreCreateOrUpdateDevice); +} + +void updateDevicesStatus() { + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + BLEdevice* p = *it; + unsigned long now = millis(); + // Check for tracker status + bool isTracker = false; +# if BLEDecoder + std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); + if (tag.length() >= 4) { + isTracker = checkIfIsTracker(tag[3]); + } + // Device tracker devices + if (isTracker) { // We apply the offline status only for tracking device, can be extended further to all the devices + if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.presenceAwayTimer) && (now > BTConfig.presenceAwayTimer)) && + (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = p->macAdr; + BLEdata["state"] = "offline"; + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + // We set the lastUpdate to 0 to avoid replublishing the offline state + p->lastUpdate = 0; + } + } + // Moving detection devices (devices with an accelerometer) + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BC08) { + if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.movingTimer) && (now > BTConfig.movingTimer)) && + (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = p->macAdr; + BLEdata["state"] = "offline"; + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + // We set the lastUpdate to 0 to avoid replublishing the offline state + p->lastUpdate = 0; + } + } +# endif + } +} + +void dumpDevices() { +# if LOG_LEVEL > LOG_LEVEL_NOTICE + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + BLEdevice* p = *it; + Log.trace(F("macAdr %s" CR), p->macAdr); + Log.trace(F("macType %d" CR), p->macType); + Log.trace(F("isDisc %d" CR), p->isDisc); + Log.trace(F("isWhtL %d" CR), p->isWhtL); + Log.trace(F("isBlkL %d" CR), p->isBlkL); + Log.trace(F("connect %d" CR), p->connect); + Log.trace(F("sensorModel_id %d" CR), p->sensorModel_id); + Log.trace(F("LastUpdate %u" CR), p->lastUpdate); + } +# endif +} + +void strupp(char* beg) { + while ((*beg = toupper(*beg))) + ++beg; +} + +# ifdef ZmqttDiscovery +void DT24Discovery(const char* mac, const char* sensorModel_id) { +# define DT24parametersCount 7 + Log.trace(F("DT24Discovery" CR)); + const char* DT24sensor[DT24parametersCount][9] = { + {"sensor", "volt", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "amp", mac, "current", jsonCurrent, "", "", "A", stateClassMeasurement}, + {"sensor", "watt", mac, "power", jsonPower, "", "", "W", stateClassMeasurement}, + {"sensor", "watt-hour", mac, "power", jsonEnergy, "", "", "kWh", stateClassMeasurement}, + {"sensor", "price", mac, "", jsonMsg, "", "", "", stateClassNone}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"binary_sensor", "inUse", mac, "power", jsonInuse, "", "", "", stateClassNone} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, DT24sensor, DT24parametersCount, "DT24", "ATorch", sensorModel_id); +} + +void BM2Discovery(const char* mac, const char* sensorModel_id) { +# define BM2parametersCount 2 + Log.trace(F("BM2Discovery" CR)); + const char* BM2sensor[BM2parametersCount][9] = { + {"sensor", "volt", mac, "voltage", jsonVoltBM2, "", "", "V", stateClassMeasurement}, // We use a json definition that retrieve only data from the BM2 decoder, as this sensor also advertize volt as an iBeacon + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, BM2sensor, BM2parametersCount, "BM2", "Generic", sensorModel_id); +} + +void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) { +# define LYWSD03MMCparametersCount 4 + Log.trace(F("LYWSD03MMCDiscovery" CR)); + const char* LYWSD03MMCsensor[LYWSD03MMCparametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, LYWSD03MMCsensor, LYWSD03MMCparametersCount, "LYWSD03MMC", "Xiaomi", sensorModel); +} + +void MHO_C401Discovery(const char* mac, const char* sensorModel) { +# define MHO_C401parametersCount 4 + Log.trace(F("MHO_C401Discovery" CR)); + const char* MHO_C401sensor[MHO_C401parametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, MHO_C401sensor, MHO_C401parametersCount, "MHO_C401", "Xiaomi", sensorModel); +} + +void HHCCJCY01HHCCDiscovery(const char* mac, const char* sensorModel) { +# define HHCCJCY01HHCCparametersCount 5 + Log.trace(F("HHCCJCY01HHCCDiscovery" CR)); + const char* HHCCJCY01HHCCsensor[HHCCJCY01HHCCparametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "lux", mac, "illuminance", jsonLux, "", "", "lx", stateClassMeasurement}, + {"sensor", "fer", mac, "", jsonFer, "", "", "µS/cm", stateClassMeasurement}, + {"sensor", "moi", mac, "", jsonMoi, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, HHCCJCY01HHCCsensor, HHCCJCY01HHCCparametersCount, "HHCCJCY01HHCC", "Xiaomi", sensorModel); +} + +void XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel) { +# define XMWSDJ04MMCparametersCount 4 + Log.trace(F("XMWSDJ04MMCDiscovery" CR)); + const char* XMWSDJ04MMCsensor[XMWSDJ04MMCparametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, XMWSDJ04MMCsensor, XMWSDJ04MMCparametersCount, "XMWSDJ04MMC", "Xiaomi", sensorModel); +} + +void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) { + Log.trace(F("xxWSD0xMMCDiscovery" CR)); + int xxWSD0xMMCparametersCount = 5; + if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_DECR") != 0) xxWSD0xMMCparametersCount = 5; + if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) xxWSD0xMMCparametersCount = 7; + const char* xxWSD0xMMCsensor[xxWSD0xMMCparametersCount][9] = { + {"sensor", "Battery", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "Temperature", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "Humidity", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement}, + {"sensor", "RSSI", mac, "signal_strength", jsonRSSI, "", "", "dB", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement, state class + }; + if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_DECR") != 0) { // Encrypted PVVX don't have Voltage + const char* voltage[9] = {"sensor", "Voltage", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}; + memcpy(&xxWSD0xMMCsensor[4], voltage, sizeof(voltage)); + }; + if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) { + const char* power[9] = {"sensor", "Power", mac, "", jsonPower, "", "", "", stateClassNone}; + memcpy(&xxWSD0xMMCsensor[5], power, sizeof(power)); + const char* open[9] = {"sensor", "Opening", mac, "", jsonOpen, "", "", "", stateClassNone}; + memcpy(&xxWSD0xMMCsensor[6], open, sizeof(open)); + }; + createDiscoveryFromList(mac, xxWSD0xMMCsensor, xxWSD0xMMCparametersCount, name, "Xiaomi", sensorModel); +} + +# else +void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) {} +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 XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel_id) {} +void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) {} +# endif + +/* + Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleScan.cpp + Ported to Arduino ESP32 by Evandro Copercini + */ +// core task implementation thanks to https://techtutorialsx.com/2017/05/09/esp32-running-code-on-a-specific-core/ + +//core on which the BLE detection task will run +static int taskCore = 0; + +class ScanCallbacks : public NimBLEScanCallbacks { + void onResult(const NimBLEAdvertisedDevice* advertisedDevice) { + NimBLEAdvertisedDevice* ad = new NimBLEAdvertisedDevice(*advertisedDevice); + if (xQueueSend(BLEQueue, &ad, 0) != pdTRUE) { + Log.error(F("BLEQueue full" CR)); + delete (ad); + } + } +} scanCallbacks; + +std::string convertServiceData(std::string deviceServiceData) { + int serviceDataLength = (int)deviceServiceData.length(); + char spr[2 * serviceDataLength + 1]; + for (int i = 0; i < serviceDataLength; i++) sprintf(spr + 2 * i, "%.2x", (unsigned char)deviceServiceData[i]); + spr[2 * serviceDataLength] = 0; + Log.trace(F("Converted service data (%d) to %s" CR), serviceDataLength, spr); + return spr; +} + +bool checkIfIsTracker(char ch) { + uint8_t data = 0; + if (ch >= '0' && ch <= '9') + data = ch - '0'; + else if (ch >= 'a' && ch <= 'f') + data = 10 + (ch - 'a'); + + if (((data >> 3) & 0x01) == 1) { + Log.trace(F("Is Device Tracker" CR)); + return true; + } else { + return false; + } +} + +void procBLETask(void* pvParameters) { + BLEAdvertisedDevice* advertisedDevice = nullptr; + + for (;;) { + xQueueReceive(BLEQueue, &advertisedDevice, portMAX_DELAY); + // Feed the watchdog + //esp_task_wdt_reset(); + if (!BTProcessLock) { + Log.trace(F("Creating BLE buffer" CR)); + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = advertisedDevice->getAddress().toString(); + BLEdata["mac_type"] = advertisedDevice->getAddress().getType(); + BLEdata["adv_type"] = advertisedDevice->getAdvType(); + Log.notice(F("BT Device detected: %s" CR), BLEdata["id"].as()); + BLEdevice* device = getDeviceByMac(BLEdata["id"].as()); + + if (BTConfig.filterConnectable && device->connect) { + Log.notice(F("Filtered connectable device" CR)); + delete (advertisedDevice); + continue; + } + + if (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(device)) && !isBlack(device))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC) + if (advertisedDevice->haveName()) + BLEdata["name"] = (char*)advertisedDevice->getName().c_str(); + if (advertisedDevice->haveManufacturerData()) { + BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString((uint8_t*)advertisedDevice->getManufacturerData().data(), + advertisedDevice->getManufacturerData().length()); + } + BLEdata["rssi"] = (int)advertisedDevice->getRSSI(); + if (advertisedDevice->haveTXPower()) + BLEdata["txpower"] = (int8_t)advertisedDevice->getTXPower(); + if (BTConfig.presenceEnable) { + hass_presence(BLEdata); // with either only sensors or not we can use it for home assistant room presence component + } + if (advertisedDevice->haveServiceData()) { + int serviceDataCount = advertisedDevice->getServiceDataCount(); + Log.trace(F("Get services data number: %d" CR), serviceDataCount); + for (int j = 0; j < serviceDataCount; j++) { + StaticJsonDocument BLEdataBufferTemp; + JsonObject BLEdataTemp = BLEdataBufferTemp.to(); + BLEdataBufferTemp = BLEdataBuffer; + std::string service_data = convertServiceData(advertisedDevice->getServiceData(j)); + Log.trace(F("Service data: %s" CR), service_data.c_str()); + std::string serviceDatauuid = advertisedDevice->getServiceDataUUID(j).toString(); + Log.trace(F("Service data UUID: %s" CR), (char*)serviceDatauuid.c_str()); + BLEdataTemp["servicedata"] = (char*)service_data.c_str(); + BLEdataTemp["servicedatauuid"] = (char*)serviceDatauuid.c_str(); + PublishDeviceData(BLEdataTemp); + } + } else { + PublishDeviceData(BLEdata); + } + } else { + Log.trace(F("Filtered MAC device" CR)); + } + updateDevicesStatus(); + } + delete (advertisedDevice); + vTaskDelay(10); + } +} + +/** + * BLEscan used to retrieve BLE advertized data from devices without connection + */ +void BLEscan() { + // Don't start the next scan until processing of previous results is complete. + while (uxQueueMessagesWaiting(BLEQueue) || queueLength != 0) { // the criteria on queueLength could be adjusted to parallelize the scan and the queue processing + delay(1); // Wait for queue to empty, a yield here instead of the delay cause the WDT to trigger + } + Log.notice(F("Scan begin" CR)); + BLEScan* pBLEScan = BLEDevice::getScan(); + pBLEScan->setScanCallbacks(&scanCallbacks); + if ((millis() > (timeBetweenActive + BTConfig.intervalActiveScan) || BTConfig.intervalActiveScan == BTConfig.BLEinterval) && !BTConfig.forcePassiveScan) { + pBLEScan->setActiveScan(true); + timeBetweenActive = millis(); + } else { + pBLEScan->setActiveScan(false); + } + pBLEScan->setInterval(BLEScanInterval); + pBLEScan->setWindow(BLEScanWindow); + NimBLEScanResults foundDevices = pBLEScan->getResults(BTConfig.scanDuration, false); + if (foundDevices.getCount()) + scanCount++; + Log.notice(F("Found %d devices, scan number %d end" CR), foundDevices.getCount(), scanCount); + Log.trace(F("Process BLE stack free: %u" CR), uxTaskGetStackHighWaterMark(xProcBLETaskHandle)); +} + +/** + * Connect to BLE devices and initiate the callbacks with a service/characteristic request + */ +# if BLEDecoder +void BLEconnect() { + if (!BTProcessLock) { + Log.notice(F("BLE Connect begin" CR)); + do { + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + BLEdevice* p = *it; + if (p->connect) { + Log.trace(F("Model to connect found: %s" CR), p->macAdr); + NimBLEAddress addr((const char*)p->macAdr, p->macType); + if (p->sensorModel_id == BLEconectable::id::LYWSD03MMC || + p->sensorModel_id == BLEconectable::id::MHO_C401) { + LYWSD03MMC_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + BLEclient.publishData(); + } else if (p->sensorModel_id == BLEconectable::id::DT24_BLE) { + DT24_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + BLEclient.publishData(); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { + BM2_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); + BLEclient.publishData(); + } else if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { + XMWSDJ04MMC_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + BLEclient.publishData(); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1) { + SBS1_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT) { + SBBT_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU) { + SBCU_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + } else { + GENERIC_connect BLEclient(addr); + if (BLEclient.processActions(BLEactions)) { + // If we don't regularly connect to this, disable connections so advertisements + // won't be filtered if BLE_FILTER_CONNECTABLE is set. + p->connect = false; + } + } + if (BLEactions.size() > 0) { + std::vector swap; + for (auto& it : BLEactions) { + if (!it.complete && --it.ttl) { + swap.push_back(it); + } else if (it.addr == NimBLEAddress(p->macAdr, p->macType)) { + if (p->sensorModel_id != BLEconectable::id::DT24_BLE && + 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 != BLEconectable::id::MHO_C401 && + p->sensorModel_id != BLEconectable::id::XMWSDJ04MMC) { + // if irregulary connected to and connection failed clear the connect flag. + p->connect = false; + } + } + } + std::swap(BLEactions, swap); + } + } + } + } while (BLEactions.size() > 0); + Log.notice(F("BLE Connect end" CR)); + } +} +# else +void BLEconnect() {} +# endif + +void stopProcessing(bool deinit) { + if (BTConfig.enabled) { + BTProcessLock = true; + // We stop the scan + Log.notice(F("Stopping BLE scan" CR)); + BLEScan* pBLEScan = BLEDevice::getScan(); + if (pBLEScan->isScanning()) { + pBLEScan->stop(); + } + + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { + Log.notice(F("Stopping BLE tasks" CR)); + //Suspending, deleting tasks and stopping BT to free memory + vTaskSuspend(xCoreTaskHandle); + vTaskDelete(xCoreTaskHandle); + vTaskSuspend(xProcBLETaskHandle); + vTaskDelete(xProcBLETaskHandle); + xSemaphoreGive(semaphoreBLEOperation); + } + // Using deinit to free memory, should only be used if we are going to restart the gateway + if (deinit) + BLEDevice::deinit(true); + } + Log.notice(F("BLE gateway stopped, free heap: %d" CR), ESP.getFreeHeap()); +} + +void coreTask(void* pvParameters) { + while (true) { + if (!BTProcessLock) { + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(30000)) == pdTRUE) { + BLEscan(); + // Launching a connect every TimeBtwConnect + if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { + timeBetweenConnect = millis(); + BLEconnect(); + } + //dumpDevices(); + Log.trace(F("CoreTask stack free: %u" CR), uxTaskGetStackHighWaterMark(xCoreTaskHandle)); + xSemaphoreGive(semaphoreBLEOperation); + } else { + Log.error(F("Failed to start scan - BLE busy" CR)); + } + if (SYSConfig.powerMode > 0) { + int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); // is this enough, it will wait the full deepsleep... + if (scan == 1) BTforceScan(); + ready_to_sleep = true; + } else { + for (int interval = BTConfig.BLEinterval, waitms; interval > 0; interval -= waitms) { + int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); + if (scan == 1) BTforceScan(); // should we break after this? + delay(waitms = interval > 100 ? 100 : interval); // 100ms + } + } + } + delay(1); + } +} + +void setupBTTasksAndBLE() { +# ifdef CONFIG_BTDM_BLE_SCAN_DUPL + BLEDevice::setScanDuplicateCacheSize(BLEScanDuplicateCacheSize); +# endif + BLEDevice::init(""); + xTaskCreateUniversal( + procBLETask, /* Function to implement the task */ + "procBLETask", /* Name of the task */ +# if defined(USE_ESP_IDF) || defined(USE_BLUFI) + 14500, +# else + 9500, /* Stack size in bytes */ +# endif + NULL, /* Task input parameter */ + 2, /* Priority of the task (set higher than core task) */ + &xProcBLETaskHandle, /* Task handle. */ + 1); /* Core where the task should run */ + + // we setup a task with priority one to avoid conflict with other gateways + xTaskCreateUniversal( + coreTask, /* Function to implement the task */ + "coreTask", /* Name of the task */ + 5120, /* Stack size in bytes */ + NULL, /* Task input parameter */ + 1, /* Priority of the task */ + &xCoreTaskHandle, /* Task handle. */ + taskCore); /* Core where the task should run */ +} + +void setupBT() { + BTConfig_init(); + BTConfig_load(); + Log.notice(F("BLE scans interval: %d" CR), BTConfig.BLEinterval); + Log.notice(F("BLE connects interval: %d" CR), BTConfig.intervalConnect); + Log.notice(F("BLE scan duration: %d" CR), BTConfig.scanDuration); + Log.notice(F("Publishing only BLE sensors: %T" CR), BTConfig.pubOnlySensors); + Log.notice(F("Publishing random MAC devices: %T" CR), BTConfig.pubRandomMACs); + Log.notice(F("Adaptive BLE scan: %T" CR), BTConfig.adaptiveScan); + Log.notice(F("Active BLE scan interval: %d" CR), BTConfig.intervalActiveScan); + Log.notice(F("minrssi: %d" CR), -abs(BTConfig.minRssi)); + Log.notice(F("Presence Away Timer: %d" CR), BTConfig.presenceAwayTimer); + Log.notice(F("Moving Timer: %d" CR), BTConfig.movingTimer); + Log.notice(F("Force passive scan: %T" CR), BTConfig.forcePassiveScan); + Log.notice(F("Enabled BLE: %T" CR), BTConfig.enabled); + + atomic_init(&forceBTScan, 0); // in theory, we don't need this + + semaphoreCreateOrUpdateDevice = xSemaphoreCreateBinary(); + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + + semaphoreBLEOperation = xSemaphoreCreateBinary(); + xSemaphoreGive(semaphoreBLEOperation); + + BLEQueue = xQueueCreate(QueueSize, sizeof(NimBLEAdvertisedDevice*)); + if (BTConfig.enabled) { + setupBTTasksAndBLE(); + Log.notice(F("gatewayBT multicore ESP32 setup done" CR)); + } else { + Log.notice(F("gatewayBT multicore ESP32 setup disabled" CR)); + } +} + +boolean valid_service_data(const char* data, int size) { + for (int i = 0; i < size; ++i) { + if (data[i] != 48) // 48 correspond to 0 in ASCII table + return true; + } + return false; +} + +# if defined(ZmqttDiscovery) && BLEDecoder == true +// This function always should be called from the main core as it generates direct mqtt messages +// When overrideDiscovery=true, we publish discovery messages of known devices (even if no new) +void launchBTDiscovery(bool overrideDiscovery) { + if (!overrideDiscovery && newDevices == 0) + return; + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdFALSE) { + Log.error(F("Semaphore NOT taken" CR)); + return; + } + newDevices = 0; + vector localDevices = devices; + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + for (vector::iterator it = localDevices.begin(); it != localDevices.end(); ++it) { + BLEdevice* p = *it; + Log.trace(F("Device mac %s" CR), p->macAdr); + // Do not launch discovery for the devices already discovered (unless we have overrideDiscovery) or that are not unique by their MAC Address (iBeacon, GAEN and Microsoft CDP) + if (overrideDiscovery || !isDiscovered(p)) { + String macWOdots = String(p->macAdr); + macWOdots.replace(":", ""); + if (p->sensorModel_id >= 0) { + Log.trace(F("Looking for Model_id: %d" CR), p->sensorModel_id); + std::string properties = decoder.getTheengProperties(p->sensorModel_id); + Log.trace(F("properties: %s" CR), properties.c_str()); + std::string brand = decoder.getTheengAttribute(p->sensorModel_id, "brand"); + std::string model = decoder.getTheengAttribute(p->sensorModel_id, "model"); + if (displayDeviceName || ForceDeviceName) { + if (p->name[0] != '\0') { + model = p->name; + } + } + std::string model_id = decoder.getTheengAttribute(p->sensorModel_id, "model_id"); + + // Check for tracker status + bool isTracker = false; + std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); + if (tag.length() >= 4) { + isTracker = checkIfIsTracker(tag[3]); + } + + String discovery_topic = String(subjectBTtoMQTT) + "/" + macWOdots; + 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 + if (isTracker) { + String tracker_name = String(model_id.c_str()) + "-tracker"; + String tracker_id = macWOdots + "-tracker"; + createDiscovery("device_tracker", + discovery_topic.c_str(), tracker_name.c_str(), 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 == TheengsDecoder::BLE_ID_NUM::BC08) { + String sensor_name = String(model_id.c_str()) + "-moving"; + String sensor_id = macWOdots + "-moving"; + createDiscovery("binary_sensor", + discovery_topic.c_str(), sensor_name.c_str(), sensor_id.c_str(), + will_Topic, "moving", "{% if value_json.get('accx') -%}on{%- else -%}off{%- endif %}", + "on", "off", "", + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } + if (displayDeviceName && p->sensorModel_id >= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_ATC && p->sensorModel_id <= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_PVVX_BTHOME_2 ) { + xxWSD0xMMCDiscovery(macWOdots.c_str(), p->name, model_id.c_str()); + } else if (!properties.empty()) { + StaticJsonDocument jsonBuffer; + auto error = deserializeJson(jsonBuffer, properties); + if (error) { + if (jsonBuffer.overflowed()) { + // This should not happen if JSON_MSG_BUFFER is large enough for + // the Theengs json properties + Log.error(F("JSON deserialization of Theengs properties overflowed (error %s), buffer capacity: %u. Program might crash. Properties json: %s" CR), + error.c_str(), jsonBuffer.capacity(), properties.c_str()); + } else { + Log.error(F("JSON deserialization of Theengs properties errored: %" CR), + error.c_str()); + } + } + for (JsonPair prop : jsonBuffer["properties"].as()) { + Log.trace(F("Key: %s"), prop.key().c_str()); + Log.trace(F("Unit: %s"), prop.value()["unit"].as()); + Log.trace(F("Name: %s"), prop.value()["name"].as()); + String entity_name = ""; + if (displayDeviceName || ForceDeviceName) { + entity_name = String(model.c_str()) + "-" + String(prop.key().c_str()); + } else { + entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); + } + String unique_id = macWOdots + "-" + String(prop.key().c_str()); + String value_template = "{{ value_json." + String(prop.key().c_str()) + " | is_defined }}"; + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1 && strcmp(prop.key().c_str(), "state") == 0) { + String payload_on = "{\"model_id\":\"X1\",\"cmd\":\"on\",\"id\":\"" + String(p->macAdr) + "\"}"; + String payload_off = "{\"model_id\":\"X1\",\"cmd\":\"off\",\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("switch", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "switch", value_template.c_str(), + payload_on.c_str(), payload_off.c_str(), "", 0, + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone, "off", "on"); + unique_id = macWOdots + "-press"; + entity_name = String(model_id.c_str()) + "-press"; + String payload_press = "{\"model_id\":\"X1\",\"cmd\":\"press\",\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("button", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "button", "", + payload_press.c_str(), "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT && strcmp(prop.key().c_str(), "open") == 0) { + value_template = "{% if value_json.direction == \"up\" -%} {{ 100 - value_json.open/2 }}{% elif value_json.direction == \"down\" %}{{ value_json.open/2 }}{% else %} {{ value_json.open/2 }}{%- endif %}"; + String command_template = "{\"model_id\":\"W270160X\",\"tilt\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("cover", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "cover", value_template.c_str(), + "50", "", "", 0, + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + "blind", nullptr, nullptr, nullptr, command_template.c_str()); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU && strcmp(prop.key().c_str(), "position") == 0) { + String command_template = "{\"model_id\":\"W070160X\",\"position\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("cover", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "cover", "{{ value_json.position }}", + "0", "100", "", 0, + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + "curtain", nullptr, nullptr, nullptr, command_template.c_str()); + } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && + strcmp(prop.key().c_str(), "weighing_mode") == 0) { + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "enum", value_template.c_str(), + "", "", prop.value()["unit"], + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassMeasurement, nullptr, nullptr, "[\"person\",\"object\"]"); + } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && + strcmp(prop.key().c_str(), "unit") == 0) { + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "enum", value_template.c_str(), + "", "", prop.value()["unit"], + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassMeasurement, nullptr, nullptr, "[\"lb\",\"kg\",\"jin\"]"); + } else if (strcmp(prop.value()["unit"], "string") == 0 && strcmp(prop.key().c_str(), "mac") != 0) { + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "", "", "", + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (p->sensorModel_id == TheengsDecoder::MUE4094RT && strcmp(prop.value()["unit"], "status") == 0) { // This device does not a broadcast when there is nothing detected so adding a timeout + createDiscovery("binary_sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "True", "False", "", + BTConfig.presenceAwayTimer / 1000, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (strcmp(prop.value()["unit"], "status") == 0) { + createDiscovery("binary_sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "True", "False", "", + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (strcmp(prop.key().c_str(), "device") != 0 && strcmp(prop.key().c_str(), "mac") != 0) { // Exception on device and mac as these ones are not sensors + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "", "", prop.value()["unit"], + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassMeasurement); + } + } + } + } 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) { + // 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"); + } + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { + // Sensor discovery + BM2Discovery(macWOdots.c_str(), "BM2"); + // Device tracker discovery + String tracker_id = macWOdots + "-tracker"; + createDiscovery("device_tracker", + discovery_topic.c_str(), "BM2-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"); + } + if (p->sensorModel_id == BLEconectable::id::MHO_C401) { + MHO_C401Discovery(macWOdots.c_str(), "MHO-C401"); + } + if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { + XMWSDJ04MMCDiscovery(macWOdots.c_str(), "XMWSDJ04MMC"); + } + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC) { + HHCCJCY01HHCCDiscovery(macWOdots.c_str(), "HHCCJCY01HHCC"); + } + } else { + Log.trace(F("Device UNKNOWN_MODEL %s" CR), p->macAdr); + } + } + } + p->isDisc = true; // we don't need the semaphore and all the search magic via createOrUpdateDevice + } else { + Log.trace(F("Device already discovered or that doesn't require discovery %s" CR), p->macAdr); + } + } +} +# else +void launchBTDiscovery(bool overrideDiscovery) {} +# endif + +# if BLEDecryptor +// ** TODO - Hex string to bytes, there is probably a function for this already just need to find it +int hexToBytes(String hex, uint8_t *out, size_t maxLen) { + int len = hex.length(); + if (len % 2 || len / 2 > maxLen) return -1; + for (int i = 0, j = 0; i < len; i += 2, j++) { + out[j] = (uint8_t) strtol(hex.substring(i, i + 2).c_str(), nullptr, 16); + } + return len / 2; +} +// Reverse bytes +void reverseBytes(uint8_t *data, size_t length) { + size_t i; + for (i = 0; i < length / 2; i++) { + uint8_t temp = data[i]; + data[i] = data[length - 1 - i]; + data[length - 1 - i] = temp; + } +} +# endif + +# if BLEDecoder +void process_bledata(JsonObject& BLEdata) { + yield(); // Necessary to let the loop run in case of connectivity issues + if (!BLEdata.containsKey("id")) { + Log.error(F("No mac address in the payload" CR)); + return; + } + const char* mac = BLEdata["id"].as(); + Log.trace(F("Processing BLE data %s" CR), BLEdata["id"].as()); + int model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); + int mac_type = BLEdata["mac_type"].as(); + +# if BLEDecryptor + if (BLEdata["encr"] && (BLEdata["encr"].as() >0 && BLEdata["encr"].as() <=2)) { + // Decrypting Encrypted BLE Data PVVX, BTHome or Victron + Log.trace(F("[BLEDecryptor] Decrypt ENCR:%d ModelID:%s Payload:%s" CR), BLEdata["encr"].as(), BLEdata["model_id"].as(), BLEdata["cipher"].as()); + + // MAC address + String macWOdots = BLEdata["id"].as(); // Mac Address without dots + macWOdots.replace(":", ""); + unsigned char macAddress[6]; + int maclen = hexToBytes(macWOdots, macAddress, 6); + if (maclen != 6) { + Log.error(F("[BLEDecryptor] Invalid MAC Address length %d" CR), maclen); + return; + } + + // AES decryption key + unsigned char bleaeskey[16]; + int bleaeskeylength = 0; + if (ble_aes_keys.containsKey(macWOdots)){ + Log.trace(F("[BLEDecryptor] Custom AES key %s" CR), ble_aes_keys[macWOdots].as()); + bleaeskeylength = hexToBytes(ble_aes_keys[macWOdots], bleaeskey, 16); + } else { + Log.trace(F("[BLEDecryptor] Default AES key" CR)); + bleaeskeylength = hexToBytes(ble_aes, bleaeskey, 16); + } + // Check AES Key + if (bleaeskeylength != 16) { + Log.error(F("[BLEDecryptor] Invalid key length %d" CR), bleaeskeylength); + return; + } + + // Build nonce and aad + uint8_t nonce[16]; + int noncelength = 0; + unsigned char aad[1]; + int aadLength; + + if (BLEdata["encr"].as() == 1){ // PVVX Encrypted + noncelength = 11; // 11 bytes + reverseBytes(macAddress, 6); // 6 bytes: device address in reverse + memcpy(nonce, macAddress, 6); + int maclen = hexToBytes(macWOdots, macAddress, 6); + + unsigned char servicedata[16]; + int servicedatalen = hexToBytes(BLEdata["servicedata"].as(), servicedata, 16); + nonce[6] = servicedatalen + 3; // 1 byte : length of (service data + type and UUID) + nonce[7] = 0x16; // 1 byte : "16" -> AD type for "Service Data - 16-bit UUID" + nonce[8] = 0x1A; // 2 bytes: "1a18" -> UUID 181a in little-endian + nonce[9] = 0x18; // + unsigned char ctr[1]; // 1 byte : counter + int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 1); + if (ctrlen != 1) { + Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); + return; + } + nonce[10] = ctr[0]; + aad[0] = 0x11; + aadLength = 1; + Log.trace(F("[BLEDecryptor] PVVX nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); + + } else if (BLEdata["encr"].as() == 2){ // BTHome V2 Encrypted + noncelength = 13; // 13 bytes + memcpy(nonce, macAddress, 6); + nonce[6] = 0xD2; // UUID + nonce[7] = 0xFC; + nonce[8] = 0x41; // BTHome Device Data encrypted payload byte + unsigned char ctr[4]; // Counter + int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 4); + if (ctrlen != 4) { + Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); + return; + } + memcpy(&nonce[9], ctr, 4); + aad[0] = 0x00; + aadLength = 0; + Log.trace(F("[BLEDecryptor] BTHomeV2 nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); + + } else if (BLEdata["encr"].as() == 3){ + nonce[16] = {0}; // Victron has a 16 byte zero padded nonce with IV bytes 6,7 + unsigned char iv[2]; + int ivlen = hexToBytes(BLEdata["ctr"].as(), iv, 2); + if (ivlen != 2) { + Log.error(F("[BLEDecryptor] Invalid iv length %d" CR), ivlen); + return; + } + memcpy(nonce, iv, 2); + Log.trace(F("[BLEDecryptor] Victron nonce %s" CR), NimBLEUtils::dataToHexString(nonce, 16).c_str()); + } else { + return; // No match + } + + // Ciphertext to bytes + int cipherlen = sizeof(BLEdata["cipher"].as()); + unsigned char ciphertext[cipherlen]; + int ciphertextlen = hexToBytes(BLEdata["cipher"].as(), ciphertext, cipherlen); + unsigned char decrypted[ciphertextlen]; // Decrypted payload + + // Decrypt ciphertext + if (BLEdata["encr"].as() == 1 || BLEdata["encr"].as() == 2) { + // Decrypt PVVX and BTHome V2 ciphertext using AES CCM + mbedtls_ccm_context ctx; + mbedtls_ccm_init(&ctx); + if (mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, bleaeskey, 128) != 0) { + Log.error(F("[BLEDecryptor] Failed to set AES key to mbedtls" CR)); + return; + } + + // Message Integrity Check (MIC) + unsigned char mic[4]; + int miclen = hexToBytes(BLEdata["mic"].as(), mic, 4); + if (miclen != 4) { + Log.error(F("[BLEDecryptor] Invalid MIC length %d" CR), miclen); + return; + } + + int ret = mbedtls_ccm_auth_decrypt( + &ctx, // AES Key + ciphertextlen, // length of ciphertext + nonce, noncelength, // Nonce + aad, aadLength, // AAD + ciphertext, // input ciphertext + decrypted, // output plaintext + mic, sizeof(mic) // Message Integrity Check + ); + mbedtls_ccm_free(&ctx); + + if (ret == 0) { + Log.notice(F("[BLEDecryptor] Decryption successful" CR)); + } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { + Log.error(F("[BLEDecryptor] Authentication failed." CR)); + return; + } else { + Log.error(F("[BLEDecryptor] Decryption failed with error: %X" CR), ret); + return; + } + + // Build new servicedata + if (BLEdata["encr"].as() == 1){ // PVVX + BLEdata["servicedata"] = NimBLEUtils::dataToHexString(decrypted, ciphertextlen); + } else if (BLEdata["encr"].as() == 2) { // BTHomeV2 + // Build new servicedata + uint8_t newservicedata[3 + ciphertextlen]; + newservicedata[0] = 0x40; // Decrypted BTHomeV2 Packet Type + newservicedata[1] = 0x00; // Packet counter which the PVVX BTHome non-encrypted has but the encrypted does not + newservicedata[2] = 0x00; // **TODO Convert the ctr to the packet counter or just stick with 0? + memcpy(&newservicedata[3], decrypted, ciphertextlen); + BLEdata["servicedata"] = NimBLEUtils::dataToHexString(newservicedata, ciphertextlen + 3); + } else { + return; + } + Log.trace(F("[BLEDecryptor] Decrypted servicedata %s" CR), BLEdata["servicedata"].as()); + + } else if (BLEdata["encr"].as() == 3) { + // Decrypt Victron Energy encrypted advertisements. + size_t nc_off = 0; + uint8_t stream_block[16] = {0}; + + mbedtls_aes_context ctx; + mbedtls_aes_init(&ctx); + mbedtls_aes_setkey_enc(&ctx, bleaeskey, 128); + int ret = mbedtls_aes_crypt_ctr( + &ctx, // AES Key + ciphertextlen, // length of ciphertext + &nc_off, + nonce, // 16 byte nonce with 2 bytes iv + stream_block, + ciphertext, // input ciphertext + decrypted // output plaintext + ); + mbedtls_aes_free(&ctx); + + if (ret == 0) { + Log.notice(F("[BLEDecryptor] Victron Decryption successful" CR)); + } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { + Log.error(F("[BLEDecryptor] Victron Authentication failed." CR)); + return; + } else { + Log.error(F("[BLEDecryptor] Victron decryption failed with error: %X" CR), ret); + return; + } + + // Build new manufacturerdata + unsigned char manufacturerdata[10 + ciphertextlen]; + int manufacturerdatalen = hexToBytes(BLEdata["manufacturerdata"].as(), manufacturerdata, 10); + manufacturerdata[2] = 0x11; // Replace byte 2 with "11" indicate decrypted data + manufacturerdata[7] = 0xff; // Replace byte 7 with "ff" to indicate decrypted data + manufacturerdata[8] = 0xff; // Replace byte 8 with "ff" to indicate decrypted data + memcpy(&manufacturerdata[8], decrypted, ciphertextlen); // Append the decrypted payload to the manufacturer data + BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString(manufacturerdata, 10 + ciphertextlen); // Rebuild manufacturerdata + Log.trace(F("[BLEDecryptor] Victron decrypted manufacturerdata %s" CR), BLEdata["manufacturerdata"].as()); + } + + // Print before and after decoder post decryption + // serializeJsonPretty(BLEdata, Serial); + model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); + // serializeJsonPretty(BLEdata, Serial); + Log.trace(F("[BLEDecryptor] Decrypted model_id %d" CR), model_id); + + // Remove the cipher fields from BLEdata + BLEdata.remove("encr"); + BLEdata.remove("cipher"); + BLEdata.remove("ctr"); + BLEdata.remove("mic"); + + } +# endif + + // Convert prmacs to RMACS until or if OMG gets Identity MAC/IRK decoding + if (BLEdata["prmac"]) { + BLEdata.remove("prmac"); + if (BLEdata["track"]) { + BLEdata.remove("track"); + } + BLEdata["type"] = "RMAC"; + Log.trace(F("Potential RMAC (prmac) converted to RMAC" CR)); + } + const char* deviceName = BLEdata["name"] | ""; + + 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 + 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 + createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); + } else { + createOrUpdateDevice(mac, device_flags_init, model_id, mac_type, deviceName); + if (BTConfig.adaptiveScan == true && (BTConfig.BLEinterval != MinTimeBtwScan || BTConfig.intervalActiveScan != MinTimeBtwScan)) { + if (BLEdata.containsKey("acts") && BLEdata.containsKey("cont")) { + if (BLEdata["acts"] && BLEdata["cont"]) { + BTConfig.BLEinterval = MinTimeBtwScan; + BTConfig.intervalActiveScan = MinTimeBtwScan; + BTConfig.scanDuration = MinScanDuration; + Log.notice(F("Active and continuous scanning required, parameters adapted" CR)); + // stateBTMeasures(false); + } + } else if (BLEdata.containsKey("cont") && BTConfig.BLEinterval != MinTimeBtwScan) { + if (BLEdata["cont"]) { + BTConfig.BLEinterval = MinTimeBtwScan; + if ((BLEdata["type"].as()).compare("CTMO") == 0) { + BTConfig.scanDuration = MinScanDuration; + } + Log.notice(F("Passive continuous scanning required, parameters adapted" CR)); + // stateBTMeasures(false); + } + } + } + } + } else { + if (BLEdata.containsKey("name")) { // Connectable only devices + std::string name = BLEdata["name"]; + if (name.compare("LYWSD03MMC") == 0) + model_id = BLEconectable::id::LYWSD03MMC; + else if (name.compare("DT24-BLE") == 0) + model_id = BLEconectable::id::DT24_BLE; + else if (name.compare("MHO-C401") == 0) + model_id = BLEconectable::id::MHO_C401; + else if (name.compare("XMWSDJ04MMC") == 0) + model_id = BLEconectable::id::XMWSDJ04MMC; + + if (model_id > 0) { + Log.trace(F("Connectable device found: %s" CR), name.c_str()); + createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); + } + } else if (BTConfig.extDecoderEnable && model_id < 0 && BLEdata.containsKey("servicedata")) { + const char* service_data = (const char*)(BLEdata["servicedata"] | ""); + if (strstr(service_data, "209800") != NULL) { + model_id = TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC; + Log.trace(F("Connectable device found: HHCCJCY01HHCC" CR)); + createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); + } + } + } + } else { + Log.trace(F("Random MAC or iBeacon device filtered" CR)); + } + if (!BTConfig.extDecoderEnable && model_id < 0) { + Log.trace(F("No eligible device found " CR)); + } +} +void PublishDeviceData(JsonObject& BLEdata) { + if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough + // Decode the payload + process_bledata(BLEdata); + // If the device is a random MAC and pubRandomMACs is false we don't publish this payload + if (!BTConfig.pubRandomMACs && (BLEdata["type"].as()).compare("RMAC") == 0) { + Log.trace(F("Random MAC, device filtered" CR)); + return; + } + // If pubAdvData is false we don't publish the adv data + if (!BTConfig.pubAdvData) { + BLEdata.remove("servicedatauuid"); + BLEdata.remove("servicedata"); + BLEdata.remove("manufacturerdata"); + BLEdata.remove("mac_type"); + BLEdata.remove("adv_type"); + // tag device properties + // BLEdata.remove("type"); type is used by the WebUI module to determine the template used to display the signal + BLEdata.remove("cidc"); + BLEdata.remove("acts"); + BLEdata.remove("cont"); + BLEdata.remove("track"); + BLEdata.remove("ctrl"); + } + // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid + if (BLEdata.containsKey("distance")) { + if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { + BLEdata["mac"] = BLEdata["id"].as(); + BLEdata["id"] = BLEdata["uuid"].as(); + } + String topic = String(mqtt_topic) + BTConfig.presenceTopic + String(gateway_name); + Log.trace(F("Pub HA Presence %s" CR), topic.c_str()); + BLEdata["topic"] = topic; + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } + + // If the device is not a sensor and pubOnlySensors is true we don't publish this payload + if (!BTConfig.pubOnlySensors || BLEdata.containsKey("model") || !BLEDecoder) { // Identified device + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } else { + Log.notice(F("Not a sensor device filtered" CR)); + return; + } + +# if BLEDecoder + if (enableMultiGTWSync && BLEdata.containsKey("model_id") && BLEdata.containsKey("id")) { + // Publish tracker sync message + bool isTracker = false; + std::string tag = decoder.getTheengAttribute(BLEdata["model_id"].as(), "tag"); + if (tag.length() >= 4) { + isTracker = checkIfIsTracker(tag[3]); + } + + if (isTracker) { + StaticJsonDocument BLEdataBuffer; + JsonObject TrackerSyncdata = BLEdataBuffer.to(); + TrackerSyncdata["gatewayid"] = gateway_name; + TrackerSyncdata["trackerid"] = BLEdata["id"].as(); + String topic = String(mqtt_topic) + String(subjectTrackerSync); + TrackerSyncdata["topic"] = topic.c_str(); + enqueueJsonObject(TrackerSyncdata); + } + } +# endif + } else { + Log.notice(F("Low rssi, device filtered" CR)); + return; + } +} +# else +void process_bledata(JsonObject& BLEdata) {} +void PublishDeviceData(JsonObject& BLEdata) { + if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough + // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid + if (BLEdata.containsKey("distance")) { + if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { + BLEdata["mac"] = BLEdata["id"].as(); + BLEdata["id"] = BLEdata["uuid"].as(); + } + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } else { + Log.notice(F("Low rssi, device filtered" CR)); + return; + } +} +# endif + +void hass_presence(JsonObject& HomePresence) { + int BLErssi = HomePresence["rssi"]; + Log.trace(F("BLErssi %d" CR), BLErssi); + int txPower = HomePresence["txpower"] | 0; + if (txPower >= 0) + txPower = -59; //if tx power is not found we set a default calibration value + Log.trace(F("TxPower: %d" CR), txPower); + double ratio = BLErssi * 1.0 / txPower; + double distance; + if (ratio < 1.0) { + distance = pow(ratio, 10); + } else { + distance = (0.89976) * pow(ratio, 7.7095) + 0.111; + } + HomePresence["distance"] = distance; + Log.trace(F("Ble distance %D" CR), distance); +} + +void BTforceScan() { + if (!BTProcessLock) { + BLEscan(); + Log.trace(F("Scan done" CR)); + if (BTConfig.bleConnect) + BLEconnect(); + } else { + Log.trace(F("Cannot launch scan due to other process running" CR)); + } +} + +void immediateBTAction(void* pvParameters) { + if (BLEactions.size()) { + // Immediate action; we need to prevent the normal connection action and stop scanning + BTProcessLock = true; + NimBLEScan* pScan = NimBLEDevice::getScan(); + if (pScan->isScanning()) { + pScan->stop(); + } + + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { + // swap the vectors so only this device is processed + std::vector dev_swap; + dev_swap.push_back(getDeviceByMac(BLEactions.back().addr.toString().c_str())); + std::swap(devices, dev_swap); + + std::vector act_swap; + act_swap.push_back(BLEactions.back()); + BLEactions.pop_back(); + std::swap(BLEactions, act_swap); + + // Unlock here to allow the action to be performed + BTProcessLock = false; + BLEconnect(); + // back to normal + std::swap(devices, dev_swap); + std::swap(BLEactions, act_swap); + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + } else { + Log.error(F("CreateOrUpdate Semaphore NOT taken" CR)); + } + + // If we stopped the scheduled connect for this action, do the scheduled now + if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { + timeBetweenConnect = millis(); + BLEconnect(); + } + xSemaphoreGive(semaphoreBLEOperation); + } else { + Log.error(F("BLE busy - immediateBTAction not sent" CR)); + gatewayState = GatewayState::ERROR; + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = BLEactions.back().addr.toString(); + BLEdata["success"] = false; + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + BLEactions.pop_back(); + BTProcessLock = false; + } + } + vTaskDelete(NULL); +} + +void startBTActionTask() { + TaskHandle_t th; + xTaskCreateUniversal( + immediateBTAction, /* Function to implement the task */ + "imActTask", /* Name of the task */ + 8000, /* Stack size in bytes */ + NULL, /* Task input parameter */ + 3, /* Priority of the task (set higher than core task) */ + &th, /* Task handle. */ + 1); /* Core where the task should run */ +} + +# if BLEDecoder +void KnownBTActions(JsonObject& BTdata) { + if (!BTdata.containsKey("id")) { + Log.error(F("BLE mac address missing" CR)); + gatewayState = GatewayState::ERROR; + return; + } + + BLEAction action{}; + action.write = true; + action.ttl = 3; + bool res = false; + if (BTdata.containsKey("model_id") && BTdata["model_id"].is()) { + if (BTdata["model_id"] == "X1") { + if (BTdata.containsKey("cmd") && BTdata["cmd"].is()) { + action.value_type = BLE_VAL_STRING; + std::string val = BTdata["cmd"].as(); // Fix #1694 + action.value = val; + createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBS1, 1); + res = true; + } + } else if (BTdata["model_id"] == "W270160X") { + if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { + action.value_type = BLE_VAL_INT; + res = true; + } else if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { + action.value_type = BLE_VAL_STRING; + res = true; + } + if (res) { + std::string val = BTdata["tilt"].as(); // Fix #1694 + action.value = val; + createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBBT, 1); + } + } else if (BTdata["model_id"] == "W070160X") { + if (BTdata.containsKey("position") && BTdata["position"].is()) { + action.value_type = BLE_VAL_INT; + res = true; + } else if (BTdata.containsKey("position") && BTdata["position"].is()) { + action.value_type = BLE_VAL_STRING; + res = true; + } + if (res) { + std::string val = BTdata["position"].as(); // Fix #1694 + action.value = val; + createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBCU, 1); + } + } + if (res) { + action.addr = NimBLEAddress(BTdata["id"].as(), 1); + BLEactions.push_back(action); + startBTActionTask(); + } else { + Log.error(F("BLE action not recognized" CR)); + gatewayState = GatewayState::ERROR; + } + } +} +# else +void KnownBTActions(JsonObject& BTdata) {} +# endif + +void XtoBTAction(JsonObject& BTdata) { + BLEAction action{}; + action.ttl = BTdata.containsKey("ttl") ? (uint8_t)BTdata["ttl"] : 1; + action.value_type = BLE_VAL_STRING; + if (BTdata.containsKey("value_type")) { + String vt = BTdata["value_type"]; + vt.toUpperCase(); + if (vt == "HEX") + action.value_type = BLE_VAL_HEX; + else if (vt == "INT") + action.value_type = BLE_VAL_INT; + else if (vt == "FLOAT") + action.value_type = BLE_VAL_FLOAT; + else if (vt != "STRING") { + Log.error(F("BLE value type invalid %s" CR), vt.c_str()); + return; + } + } + + Log.trace(F("BLE ACTION TTL = %u" CR), action.ttl); + action.complete = false; + if (BTdata.containsKey("ble_write_address") && + BTdata.containsKey("ble_write_service") && + BTdata.containsKey("ble_write_char") && + BTdata.containsKey("ble_write_value")) { + action.addr = NimBLEAddress(BTdata["ble_write_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); + action.service = NimBLEUUID((const char*)BTdata["ble_write_service"]); + action.characteristic = NimBLEUUID((const char*)BTdata["ble_write_char"]); + std::string val = BTdata["ble_write_value"].as(); // Fix #1694 + action.value = val; + action.write = true; + Log.trace(F("BLE ACTION Write" CR)); + } else if (BTdata.containsKey("ble_read_address") && + BTdata.containsKey("ble_read_service") && + BTdata.containsKey("ble_read_char")) { + action.addr = NimBLEAddress(BTdata["ble_read_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); + action.service = NimBLEUUID((const char*)BTdata["ble_read_service"]); + action.characteristic = NimBLEUUID((const char*)BTdata["ble_read_char"]); + action.write = false; + Log.trace(F("BLE ACTION Read" CR)); + } else { + return; + } + + createOrUpdateDevice(action.addr.toString().c_str(), device_flags_connect, UNKWNON_MODEL, action.addr.getType()); + + BLEactions.push_back(action); + if (BTdata.containsKey("immediate") && BTdata["immediate"].as()) { + startBTActionTask(); + } +} + +void XtoBT(const char* topicOri, JsonObject& BTdata) { // json object decoding + if (cmpToMainTopic(topicOri, subjectMQTTtoBTset)) { + Log.trace(F("MQTTtoBT json set" CR)); + + // Black list & white list set + bool WorBupdated; + WorBupdated = updateWorB(BTdata, true); + WorBupdated |= updateWorB(BTdata, false); + + if (WorBupdated) { + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { + //dumpDevices(); + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + } + } + + // Force scan now + if (BTdata.containsKey("interval") && BTdata["interval"] == 0) { + Log.notice(F("BLE forced scan" CR)); + atomic_store_explicit(&forceBTScan, 1, ::memory_order_seq_cst); // ask the other core to do the scan for us + } + + /* + * Configuration modifications priorities: + * First `init=true` and `load=true` commands are executed (if both are present, INIT prevails on LOAD) + * Then parameters included in json are taken in account + * Finally `erase=true` and `save=true` commands are executed (if both are present, ERASE prevails on SAVE) + */ + if (BTdata.containsKey("init") && BTdata["init"].as()) { + // Restore the default (initial) configuration + BTConfig_init(); + } else if (BTdata.containsKey("load") && BTdata["load"].as()) { + // Load the saved configuration, if not initialised + BTConfig_load(); + } + + // Load config from json if available + BTConfig_fromJson(BTdata); + + } else if (cmpToMainTopic(topicOri, subjectMQTTtoBT)) { + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { + KnownBTActions(BTdata); + XtoBTAction(BTdata); + xSemaphoreGive(semaphoreBLEOperation); + } else { + Log.error(F("BLE busy - BTActions not sent" CR)); + gatewayState = GatewayState::ERROR; + } + } else if (strstr(topicOri, subjectTrackerSync) != NULL) { + if (BTdata.containsKey("gatewayid") && BTdata.containsKey("trackerid") && BTdata["gatewayid"] != gateway_name) { + BLEdevice* device = getDeviceByMac(BTdata["trackerid"].as()); + if (device != &NO_BT_DEVICE_FOUND && device->lastUpdate != 0) { + device->lastUpdate = 0; + Log.notice(F("Tracker %s disassociated by gateway %s" CR), BTdata["trackerid"].as(), BTdata["gatewayid"].as()); + } + } + } +} +#endif diff --git a/main/mqttDiscovery.cpp b/main/mqttDiscovery.cpp index d60d2be81a..89da508bac 100644 --- a/main/mqttDiscovery.cpp +++ b/main/mqttDiscovery.cpp @@ -1,1567 +1,1568 @@ -/* - OpenMQTTGateway Addon - ESP8266 or Arduino program for home automation - - Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker - Send and receiving command by MQTT - - This is the Home Assistant MQTT Discovery addon. - - Copyright: (c) Rafal Herok - - This file is part of OpenMQTTGateway. - - OpenMQTTGateway is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - OpenMQTTGateway is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#include "User_config.h" - -#ifdef ZmqttDiscovery -# include "TheengsCommon.h" - -# ifdef ESP8266 -# include -# elif defined(ESP32) -# include - -# include "esp_mac.h" -# endif -# ifdef ESP32_ETHERNET -# include -# endif -# include "config_mqttDiscovery.h" - -extern bool ethConnected; -extern JsonArray modules; - -char discovery_prefix[parameters_size + 1] = discovery_Prefix; -// From https://github.com/home-assistant/core/blob/d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf/homeassistant/const.py#L225 -// List of classes available in Home Assistant -const char* availableHASSClasses[] = {"battery_charging", - "battery", - "carbon_dioxide", - "carbon_monoxide", - "connectivity", - "current", - "data_size", - "distance", - "door", - "duration", - "energy", - "enum", - "frequency", - "gas", - "humidity", - "illuminance", - "irradiance", - "lock", - "motion", - "moving", - "occupancy", - "pm1", - "pm10", - "pm25", - "power_factor", - "power", - "precipitation_intensity", - "precipitation", - "pressure", - "problem", - "restart", - "signal_strength", - "sound_pressure", - "temperature", - "timestamp", - "voltage", - "water", - "weight", - "wind_speed", - "window"}; - -// From https://github.com/home-assistant/core/blob/d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf/homeassistant/const.py#L379 -// List of units available in Home Assistant -const char* availableHASSUnits[] = {"A", - "B", - "UV index", - "V", - "W", - "W", - "bpm", - "bar", - "cm", - "dB", - "dBm", - "ft", - "h", - "hPa", - "Hz", - "kg", - "kW", - "kWh", - "km/h", - "lb", - "lx", - "m/s", - "m/s²", - "m³", - "mg/m³", - "min", - "mm", - "mm/h", - "ms", - "mV", - "µS/cm", - "μg/m³", - "Ω", - "%", - "°", - "°C", - "°F", - "s", - "wb²" -}; - -String getMacAddress() { - uint8_t baseMac[6]; - char baseMacChr[13] = {0}; -# if defined(ESP8266) - WiFi.macAddress(baseMac); - sprintf(baseMacChr, "%02X%02X%02X%02X%02X%02X", baseMac[0], baseMac[1], baseMac[2], baseMac[3], baseMac[4], baseMac[5]); -# elif defined(ESP32) - esp_read_mac(baseMac, ESP_MAC_WIFI_STA); - sprintf(baseMacChr, "%02X%02X%02X%02X%02X%02X", baseMac[0], baseMac[1], baseMac[2], baseMac[3], baseMac[4], baseMac[5]); -# else - sprintf(baseMacChr, "%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); -# endif - return String(baseMacChr); -} - -String getUniqueId(String name, String sufix) { - String uniqueId = (String)getMacAddress() + "-" + name + sufix; - return String(uniqueId); -} - -# if defined(ZgatewayBT) || defined(SecondaryModule) -# include "config_BT.h" -/** - * Create a discover messages form a list of attribute - * - * @param mac the MAC address - * @param sensorList[][0] = component type - * @param sensorList[][1] = name - * @param sensorList[][2] = availability topic - * @param sensorList[][3] = device class - * @param sensorList[][4] = value template - * @param sensorList[][5] = payload on - * @param sensorList[][6] = payload off - * @param sensorList[][7] = unit of measurement - * @param sensorList[][8] = unit of measurement - * @param sensorCount number of sensor - * @param device_name name of sensors - * @param device_manufacturer name of manufacturer - * @param device_model the model - * */ -void createDiscoveryFromList(const char* mac, - const char* sensorList[][9], - int sensorCount, - const char* device_name, - const char* device_manufacturer, - const char* device_model) { - for (int i = 0; i < sensorCount; i++) { - String discovery_topic = String(subjectBTtoMQTT) + "/" + String(mac); - String unique_id = String(mac) + "-" + sensorList[i][1]; - - createDiscovery(sensorList[i][0], - discovery_topic.c_str(), sensorList[i][1], unique_id.c_str(), - will_Topic, sensorList[i][3], sensorList[i][4], - sensorList[i][5], sensorList[i][6], sensorList[i][7], - 0, "", "", false, "", - device_name, device_manufacturer, device_model, mac, false, - sensorList[i][8] //The state class - ); - } -} -# endif - -/** - * @brief Announce that the Gateway have the ability to raise Trigger. - * This function provide the configuration of the MQTT Device trigger ( @see https://www.home-assistant.io/integrations/device_trigger.mqtt/ ). - * All messages published by this function will be interpreted as configuration messages of Gateway Triggers. - * Instead, all messages published on the "triggerTopic" will be interpreted as Gateway trigger. - * - * @param triggerTopic Mandatory - The MQTT topic subscribed to receive trigger events. - * @param type The type of the trigger, e.g. button_short_press. Entries supported by the HA Frontend: button_short_press, button_short_release, button_long_press, button_long_release, button_double_press, button_triple_press, button_quadruple_press, button_quintuple_press. If set to an unsupported value, will render as subtype type, e.g. button_1 spammed with type set to spammed and subtype set to button_1 - * @param subtype The subtype of the trigger, e.g. button_1. Entries supported by the HA frontend: turn_on, turn_off, button_1, button_2, button_3, button_4, button_5, button_6. If set to an unsupported value, will render as subtype type, e.g. left_button pressed with type set to button_short_press and subtype set to left_button - * @param object_id The object_id of the trigger. - * @param value_template The template to render the value of the trigger. The template can use the variables trigger.id, trigger.type, trigger.subtype, trigger.payload, trigger.payload_json, trigger.topic, trigger.timestamp, trigger.value, trigger.value_json. The template can be a string or a JSON object. If the template is a JSON object, it must be a valid JSON object. If the template is a string, it will be rendered as a string. If the template is a JSON object, it will be rendered as a JSON object. - */ -void announceGatewayTrigger(const char* triggerTopic, - const char* type, - const char* subtype, - const char* object_id, - const char* value_template) { - //Create The Json - StaticJsonDocument jsonBuffer; - JsonObject sensor = jsonBuffer.to(); - - /** - * The type of automation, must be ‘trigger’. - * @see https://www.home-assistant.io/integrations/device_trigger.mqtt/#automation_type - */ - sensor["automation_type"] = "trigger"; - - /** - * Must be device_automation. Only allowed and required in MQTT auto discovery device messages. - * @see https://www.home-assistant.io/integrations/device_trigger.mqtt/#platform - * @see https://www.home-assistant.io/integrations/mqtt/#device-discovery-payload - */ - sensor["platform "] = "device_automation"; - - // The MQTT topic subscribed to receive trigger events. - if (triggerTopic && triggerTopic[0]) { - char state_topic[mqtt_topic_max_size]; - - strcpy(state_topic, mqtt_topic); - strcat(state_topic, gateway_name); - strcat(state_topic, triggerTopic); - - /** - * "info_topic" is not a standard field, for the message is required the filed "topic", but this filed is reserved and it is used to know where to publish the topic. - * If we want to send on the message the topic information is usefull to use this "info_topic" that will be not delete by the send function but converted to "topic" - */ - sensor["info_topic"] = state_topic; - } else { - Log.error(F("[RF] Error: topic is mandatory for device trigger Discovery" CR)); - return; - } - - /** - * The type of the trigger, e.g. button_short_press. - * Entries supported by the HA frontend: button_short_press, button_short_release, button_long_press, button_long_release, button_double_press, button_triple_press, button_quadruple_press, button_quintuple_press. - * If set to an unsupported value, will render as subtype type, e.g. button_1 spammed with type set to spammed and subtype set to button_1 - */ - if (type && type[0] != 0) { - sensor["type"] = type; - } else { - sensor["type"] = "button_short_press"; - } - - /** - * The subtype of the trigger, e.g. turn_on. - * Entries supported by the frontend: turn_on, turn_off, button_1, button_2, button_3, button_4, button_5, button_6. - * If set to an unsupported value, will render as subtype type, e.g. left_button pressed with type set to button_short_press and subtype set to left_button - */ - if (subtype && subtype[0] != 0) { - sensor["subtype"] = subtype; - } else { - sensor["subtype"] = "turn_on"; - } - - // ------------------ START DEVICE DECLARATION -------------------------------------------------- - // TODO: This section, like the almost identical one in createDiscovery, should be placed in a - // separate function and managed specifically to avoid errors in representing the device - // in the HASS world. - // ------------------------------------------------------------------------------------------------- - - // Information about the device: this device trigger is a part of to tie it into the HA device registry. - StaticJsonDocument jsonDeviceBuffer; - JsonObject device = jsonDeviceBuffer.to(); - - // A link to the webpage that can manage the configuration of this device. - if (ethConnected) { -# ifdef ESP32_ETHERNET - device["configuration_url"] = String("http://") + String(ETH.localIP().toString()) + String("/"); //configuration_url -# endif - } else { - device["configuration_url"] = String("http://") + String(WiFi.localIP().toString()) + String("/"); //configuration_url - } - - /* - * A list of connections of the device to the outside world as a list of tuples [connection_type, connection_identifier]. - * For example the MAC address of a network interface: "connections": [["mac", "02:5b:26:a8:dc:12"]]. - */ - JsonArray connections = device.createNestedArray("connections"); - JsonArray connection_mac = connections.createNestedArray(); - connection_mac.add("mac"); - connection_mac.add(getMacAddress()); - - // A list of IDs that uniquely identify the device. For example a serial number. - String unique_id = String(getMacAddress()); - JsonArray identifiers = device.createNestedArray("identifiers"); - identifiers.add(unique_id); - - // The manufacturer of the device. - device["mf"] = GATEWAY_MANUFACTURER; - - // The model of the device. -# ifndef GATEWAY_MODEL - String model = ""; - serializeJson(modules, model); - device["mdl"] = model; -# else - device["mdl"] = GATEWAY_MODEL; -# endif - - // The name of the device. - device["name"] = String(gateway_name); - device["sw"] = OMG_VERSION; - // ------------------ END DEVICE DECLARATION ------------------ // - - sensor["device"] = device; //device representing the board - - if (value_template && value_template[0]) { - sensor["value_template"] = String(value_template); - } - - /* Publish on the topic - The discovery topic needs to be: /device_automation/[/]/config. - - Note that only one trigger may be defined per unique discovery topic. - Also note that the combination of type and subtype should be unique for a device. - - */ - - String topic_to_publish = String(discovery_prefix) + "/device_automation/" + String(unique_id) + "/" + object_id + "/config"; - Log.trace(F("Announce Gatewy Trigger %s" CR), topic_to_publish.c_str()); - sensor["topic"] = topic_to_publish; - sensor["retain"] = true; - enqueueJsonObject(sensor); -} - -/* - * Remove a substring p from a given string s -*/ -std::string remove_substring(std::string s, const std::string& p) { - std::string::size_type n = p.length(); - - for (std::string::size_type i = s.find(p); - i != std::string::npos; - i = s.find(p)) - s.erase(i, n); - - return s; -} - -/** - * @brief Generate message and publish it on an MQTT discovery explorer. For HA @see https://www.home-assistant.io/docs/mqtt/discovery/ - * - * @param sensor_type the Type - * @param st_topic set state topic, - * @param s_name set name, - * @param unique_id set uniqueId - * @param availability_topic set availability_topic, - * @param device_class set device_class, - * @param value_template set value_template, - * @param payload_on set payload_on, - * @param payload_off set payload_off, - * @param unit_of_meas set unit_of_meas, - * @param off_delay set off_delay - * @param payload_available set payload_available, - * @param payload_not_available set payload_not_available - * @param gateway_entity set is a gateway entity, - * @param cmd_topic set command topic - * @param device_name set device name, - * @param device_manufacturer set device manufacturer, - * @param device_model set device model, - * @param device_id set device(BLE)/entity(RTL_433) identification, - * @param retainCmd set retain - * @param state_class set state class - * - * */ -void createDiscovery(const char* sensor_type, - const char* st_topic, const char* s_name, const char* unique_id, - const char* availability_topic, const char* device_class, const char* value_template, - const char* payload_on, const char* payload_off, const char* unit_of_meas, - int off_delay, - const char* payload_available, const char* payload_not_available, bool gateway_entity, const char* cmd_topic, - const char* device_name, const char* device_manufacturer, const char* device_model, const char* device_id, bool retainCmd, - const char* state_class, const char* state_off, const char* state_on, const char* enum_options, const char* command_template) { - StaticJsonDocument jsonBuffer; - JsonObject sensor = jsonBuffer.to(); - - // If a component cannot render it's state (f.i. KAKU relays) no state topic - // should be added. Without a state topic HA will use optimistic mode for the - // component by default. The Home Assistant UI for optimistic switches - // (separate on and off icons) allows for multiple - // subsequent on commands. This is required for dimming on KAKU relays like - // the ACM-300. - if (st_topic && st_topic[0]) { - char state_topic[mqtt_topic_max_size]; - // If not an entity belonging to the gateway we put wild card for the location and gateway name - // allowing to have the entity detected by several gateways and a consistent discovery topic among the gateways - if (gateway_entity) { - strcpy(state_topic, mqtt_topic); - strcat(state_topic, gateway_name); - } else { - strcpy(state_topic, "+/+"); - } - strcat(state_topic, st_topic); - if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { - sensor["tilt_status_t"] = state_topic; // tilt_status_topic for blind - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { - sensor["pos_t"] = state_topic; // position_topic for curtain - } else { - sensor["stat_t"] = state_topic; - } - } - - if (availability_topic && availability_topic[0] && gateway_entity) { - char avty_topic[mqtt_topic_max_size]; - strcpy(avty_topic, mqtt_topic); - strcat(avty_topic, gateway_name); - strcat(avty_topic, availability_topic); - sensor["avty_t"] = avty_topic; - } - - if (device_class && device_class[0]) { - // We check if the class belongs to HAAS classes list - int num_classes = sizeof(availableHASSClasses) / sizeof(availableHASSClasses[0]); - for (int i = 0; i < num_classes; i++) { // see class list and size into config_mqttDiscovery.h - if (strcmp(availableHASSClasses[i], device_class) == 0) { - sensor["dev_cla"] = device_class; //device_class - } - } - } - - if (unit_of_meas && unit_of_meas[0]) { - // We check if the class belongs to HAAS units list - int num_units = sizeof(availableHASSUnits) / sizeof(availableHASSUnits[0]); - for (int i = 0; i < num_units; i++) { // see units list and size into config_mqttDiscovery.h - if (strcmp(availableHASSUnits[i], unit_of_meas) == 0) { - sensor["unit_of_meas"] = unit_of_meas; //unit_of_measurement*/ - } - } - } - sensor["name"] = s_name; //name - sensor["uniq_id"] = unique_id; //unique_id - if (retainCmd) - sensor["retain"] = retainCmd; // Retain command - if (value_template && value_template[0]) { - if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { - sensor["tilt_status_tpl"] = value_template; // tilt_status_template for blind - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { - sensor["pos_tpl"] = value_template; // position_template for curtain - } else { - sensor["val_tpl"] = value_template; //HA Auto discovery - } - } - if (payload_on && payload_on[0]) { - if (strcmp(sensor_type, "button") == 0) { - sensor["pl_prs"] = payload_on; // payload_press for a button press - } else if (strcmp(sensor_type, "number") == 0) { - sensor["cmd_tpl"] = payload_on; // payload_on for a switch - } else if (strcmp(sensor_type, "update") == 0) { - sensor["pl_inst"] = payload_on; // payload_install for update - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { - int value = std::stoi(payload_on); - sensor["tilt_opnd_val"] = value; // tilt_open_value for blind - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { - int value = std::stoi(payload_on); - sensor["pos_open"] = value; // open value for curtain - } else { - if (strcmp(payload_on, "True") == 0 || strcmp(payload_on, "true") == 0) { - sensor["pl_on"] = true; - } else { - sensor["pl_on"] = payload_on; // payload_on for the rest - } - } - } - if (payload_off && payload_off[0]) { - if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { - sensor["pl_cls"] = payload_off; // payload_close for cover - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { - int value = std::stoi(payload_off); - sensor["pos_clsd"] = value; // closed value for curtain - } else { - if (strcmp(payload_off, "False") == 0 || strcmp(payload_off, "false") == 0) { - sensor["pl_off"] = false; - } else { - sensor["pl_off"] = payload_off; //payload_off for the rest - } - } - } - if (command_template && command_template[0]) { - if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { - sensor["tilt_cmd_tpl"] = command_template; //command_template - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { - sensor["set_pos_tpl"] = command_template; //command_template - } else { - sensor["cmd_tpl"] = command_template; //command_template - } - } - if (strcmp(sensor_type, "device_tracker") == 0) - sensor["source_type"] = "bluetooth_le"; // payload_install for update - if (off_delay != 0) - sensor["off_delay"] = off_delay; //off_delay - if (payload_available[0]) - sensor["pl_avail"] = payload_available; // payload_on - if (payload_not_available[0]) - sensor["pl_not_avail"] = payload_not_available; //payload_off - if (state_class && state_class[0]) - sensor["stat_cla"] = state_class; //add the state class on the sensors ( https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes ) - if (state_on != nullptr) - if (strcmp(state_on, "true") == 0) { - sensor["stat_on"] = true; - } else { - sensor["stat_on"] = state_on; - } - if (state_off != nullptr) - if (strcmp(state_off, "false") == 0) { - sensor["stat_off"] = false; - } else { - sensor["stat_off"] = state_off; - } - if (cmd_topic[0]) { - char command_topic[mqtt_topic_max_size]; - strcpy(command_topic, mqtt_topic); - strcat(command_topic, gateway_name); - strcat(command_topic, cmd_topic); - if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { - sensor["tilt_cmd_t"] = command_topic; // tilt_command_topic for cover - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { - sensor["set_pos_t"] = command_topic; // position_command_topic for curtain - } else { - sensor["cmd_t"] = command_topic; //command_topic - } - } - - if (enum_options != nullptr) { - sensor["options"] = enum_options; - } - - StaticJsonDocument jsonDeviceBuffer; - JsonObject device = jsonDeviceBuffer.to(); - JsonArray identifiers = device.createNestedArray("ids"); - - if (gateway_entity) { - //device representing the board - device["name"] = String(gateway_name); -# ifndef GATEWAY_MODEL - String model = ""; - serializeJson(modules, model); - device["mdl"] = model; -# else - device["mdl"] = GATEWAY_MODEL; -# endif - device["mf"] = GATEWAY_MANUFACTURER; - if (ethConnected) { -# ifdef ESP32_ETHERNET - device["cu"] = String("http://") + String(ETH.localIP().toString()) + String("/"); //configuration_url -# endif - } else { - device["cu"] = String("http://") + String(WiFi.localIP().toString()) + String("/"); //configuration_url - } - - device["sw"] = OMG_VERSION; - identifiers.add(String(getMacAddress())); - } else { - //The Connections - if (device_id[0]) { - JsonArray connections = device.createNestedArray("cns"); - JsonArray connection_mac = connections.createNestedArray(); - connection_mac.add("mac"); - connection_mac.add(device_id); - //Device representing the actual sensor/switch device - //The Device ID - identifiers.add(device_id); - } - - if (device_manufacturer[0]) { - device["mf"] = device_manufacturer; - } - - if (device_model[0]) { - device["mdl"] = device_model; - } - - // generate unique device name by adding the second half of the device_id only if device_name and device_id are different and we don't want to use the BLE name - if (device_name[0]) { - #if defined(ZgatewayBT) // displayDeviceName only applies when running Bluetooth - if (strcmp(device_id, device_name) != 0 && device_id[0] && !displayDeviceName) { - #else !ForceDeviceName // Support ForceDeviceName for esp8266's - if (strcmp(device_id, device_name) != 0 && device_id[0] && !ForceDeviceName) { - #endif - device["name"] = device_name + String("-") + String(device_id + 6); - } else { - device["name"] = device_name; - } - } - - device["via_device"] = String(getMacAddress()); //mac address of the gateway so that the devices link to the gateway - } - - sensor["device"] = device; - - String topic = String(discovery_prefix) + "/" + String(sensor_type) + "/" + String(unique_id) + "/config"; - Log.trace(F("Announce Device %s on %s" CR), String(sensor_type).c_str(), topic.c_str()); - sensor["topic"] = topic; - sensor["retain"] = true; - enqueueJsonObject(sensor); -} - -void eraseTopic(const char* sensor_type, const char* unique_id) { - if (sensor_type == NULL || unique_id == NULL) { - return; - } - String topic = String(discovery_prefix) + "/" + String(sensor_type) + "/" + String(unique_id) + "/config"; - Log.trace(F("Erase entity discovery %s on %s" CR), String(sensor_type).c_str(), topic.c_str()); - pubMQTT((char*)topic.c_str(), "", true); -} - -# if defined(ZgatewayBT) || defined(SecondaryModule) -void btPresenceParametersDiscovery() { - createDiscovery("number", //set Type - subjectBTtoMQTT, "BT: Presence/Tracker timeout", (char*)getUniqueId("presenceawaytimer", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.presenceawaytimer/60000 }}", //set availability_topic,device_class,value_template, - "{\"presenceawaytimer\":{{value*60000}},\"save\":true}", "", "min", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, - stateClassNone //State Class - ); -} -void btScanParametersDiscovery() { - createDiscovery("number", //set Type - subjectBTtoMQTT, "BT: Interval between scans", (char*)getUniqueId("interval", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.interval/1000 }}", //set availability_topic,device_class,value_template, - "{\"interval\":{{value*1000}},\"save\":true}", "", "s", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, - stateClassNone //State Class - ); - createDiscovery("number", //set Type - subjectBTtoMQTT, "BT: Interval between active scans", (char*)getUniqueId("intervalacts", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.intervalacts/1000 }}", //set availability_topic,device_class,value_template, - "{\"intervalacts\":{{value*1000}},\"save\":true}", "", "s", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, - stateClassNone //State Class - ); -} -# endif - -void pubMqttDiscovery() { - Log.trace(F("omgStatusDiscovery" CR)); -# ifdef SecondaryModule - String uptimeName = "SYS: Uptime " + String(SecondaryModule); - String uptimeId = "uptime-" + String(SecondaryModule); - createDiscovery("sensor", //set Type - subjectSYStoMQTTSecondaryModule, uptimeName.c_str(), (char*)getUniqueId(uptimeId, "").c_str(), //set state_topic,name,uniqueId - will_Topic, "duration", "{{ value_json.uptime }}", //set availability_topic,device_class,value_template, - "", "", "s", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - String freememName = "SYS: Free memory " + String(SecondaryModule); - String freememId = "freemem-" + String(SecondaryModule); - createDiscovery("sensor", //set Type - subjectSYStoMQTTSecondaryModule, freememName.c_str(), (char*)getUniqueId(freememId, "").c_str(), //set state_topic,name,uniqueId - will_Topic, "data_size", "{{ value_json.freemem }}", //set availability_topic,device_class,value_template, - "", "", "B", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - String restartName = "SYS: Restart " + String(SecondaryModule); - String restartId = "restart-" + String(SecondaryModule); - createDiscovery("button", //set Type - will_Topic, restartName.c_str(), (char*)getUniqueId(restartId, "").c_str(), //set state_topic,name,uniqueId - will_Topic, "restart", "", //set availability_topic,device_class,value_template, - "{\"cmd\":\"restart\"}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSsetSecondaryModule, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - createDiscovery("binary_sensor", //set Type - will_Topic, "SYS: Connectivity", (char*)getUniqueId("connectivity", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "connectivity", "", //set availability_topic,device_class,value_template, - Gateway_AnnouncementMsg, will_Message, "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Uptime", (char*)getUniqueId("uptime", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "duration", "{{ value_json.uptime }}", //set availability_topic,device_class,value_template, - "", "", "s", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Free memory", (char*)getUniqueId("freemem", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "data_size", "{{ value_json.freemem }}", //set availability_topic,device_class,value_template, - "", "", "B", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: IP", (char*)getUniqueId("ip", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.ip }}", //set availability_topic,device_class,value_template, - "", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("switch", //set Type - subjectSYStoMQTT, "SYS: Auto discovery", (char*)getUniqueId("disc", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.disc }}", //set availability_topic,device_class,value_template, - "{\"disc\":true,\"save\":true}", "{\"disc\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_avalaible,payload_not avalaible ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain, - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); -# ifdef LED_ADDRESSABLE - createDiscovery("number", //set Type - subjectSYStoMQTT, "SYS: LED Brightness", (char*)getUniqueId("rgbb", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ (value_json.rgbb/2.55) | round(0) }}", //set availability_topic,device_class,value_template, - "{\"rgbb\":{{ (value*2.55) | round(0) }},\"save\":true}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, - stateClassNone //State Class - ); -# endif - -# ifdef ZdisplaySSD1306 -# include "config_SSD1306.h" - createDiscovery("switch", //set Type - subjectSSD1306toMQTT, "SSD1306: Control", (char*)getUniqueId("onstate", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.onstate }}", //set availability_topic,device_class,value_template, - "{\"onstate\":true,\"save\":true}", "{\"onstate\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSSD1306set, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - createDiscovery("switch", //set Type - subjectWebUItoMQTT, "SSD1306: Display metric", (char*)getUniqueId("displayMetric", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.displayMetric }}", //set availability_topic,device_class,value_template, - "{\"displayMetric\":true,\"save\":true}", "{\"displayMetric\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoWebUIset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - createDiscovery("number", //set Type - subjectSSD1306toMQTT, "SSD1306: Brightness", (char*)getUniqueId("brightness", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.brightness }}", //set availability_topic,device_class,value_template, - "{\"brightness\":{{value}},\"save\":true}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSSD1306set, //set,payload_available,payload_not available,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifndef ESP32_ETHERNET - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: RSSI", (char*)getUniqueId("rssi", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "signal_strength", "{{ value_json.rssi }}", //set availability_topic,device_class,value_template, - "", "", "dB", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif -# if defined(ESP32) && !defined(NO_INT_TEMP_READING) - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Internal temperature", (char*)getUniqueId("tempc", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "temperature", "{{ value_json.tempc | round(1)}}", //set availability_topic,device_class,value_template, - "", "", "°C", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_avalaible,payload_not avalaible ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC - stateClassMeasurement //State Class - ); -# if defined(ZboardM5STICKC) || defined(ZboardM5STICKCP) || defined(ZboardM5TOUGH) - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Bat voltage", (char*)getUniqueId("m5batvoltage", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "voltage", "{{ value_json.m5batvoltage }}", //set availability_topic,device_class,value_template, - "", "", "V", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Bat current", (char*)getUniqueId("m5batcurrent", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "current", "{{ value_json.m5batcurrent }}", //set availability_topic,device_class,value_template, - "", "", "A", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Vin voltage", (char*)getUniqueId("m5vinvoltage", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "voltage", "{{ value_json.m5vinvoltage }}", //set availability_topic,device_class,value_template, - "", "", "V", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Vin current", (char*)getUniqueId("m5vincurrent", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "current", "{{ value_json.m5vincurrent }}", //set availability_topic,device_class,value_template, - "", "", "A", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif -# ifdef ZboardM5STACK - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Batt level", (char*)getUniqueId("m5battlevel", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "battery", "{{ value_json.m5battlevel }}", //set availability_topic,device_class,value_template, - "", "", "%", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("binary_sensor", //set Type - subjectSYStoMQTT, "SYS: Is Charging", (char*)getUniqueId("m5ischarging", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "{{ value_json.m5ischarging }}", "", //set availability_topic,device_class,value_template, - "", "", "%", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("binary_sensor", //set Type - subjectSYStoMQTT, "SYS: Is Charge Full", (char*)getUniqueId("m5ischargefull", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "{{ value_json.m5ischargefull }}", "", //set availability_topic,device_class,value_template, - "", "", "%", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif -# endif - createDiscovery("button", //set Type - will_Topic, "SYS: Restart gateway", (char*)getUniqueId("restart", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "restart", "", //set availability_topic,device_class,value_template, - "{\"cmd\":\"restart\"}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("button", //set Type - will_Topic, "SYS: Erase credentials", (char*)getUniqueId("erase", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "", //set availability_topic,device_class,value_template, - "{\"cmd\":\"erase\"}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# ifdef MQTT_HTTPS_FW_UPDATE - createDiscovery("update", //set Type - subjectRLStoMQTT, "SYS: Firmware Update", (char*)getUniqueId("update", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "firmware", "", //set availability_topic,device_class,value_template, - LATEST_OR_DEV, "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSupdate, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZsensorBME280 -# include "config_BME280.h" -# define BMEparametersCount 5 - Log.trace(F("bme280Discovery" CR)); - char* BMEsensor[BMEparametersCount][8] = { - {"sensor", "temp", "bme", "temperature", jsonTempc, "", "", "°C"}, - {"sensor", "pa", "bme", "pressure", jsonPa, "", "", "hPa"}, - {"sensor", "hum", "bme", "humidity", jsonHum, "", "", "%"}, - {"sensor", "altim", "bme", "", jsonAltim, "", "", "m"}, - {"sensor", "altift", "bme", "", jsonAltif, "", "", "ft"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < BMEparametersCount; i++) { - createDiscovery(BMEsensor[i][0], - BMETOPIC, BMEsensor[i][1], (char*)getUniqueId(BMEsensor[i][1], BMEsensor[i][2]).c_str(), - will_Topic, BMEsensor[i][3], BMEsensor[i][4], - BMEsensor[i][5], BMEsensor[i][6], BMEsensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - } -# endif - -# ifdef ZsensorHTU21 -# include "config_HTU21.h" -# define HTUparametersCount 2 - Log.trace(F("htu21Discovery" CR)); - char* HTUsensor[HTUparametersCount][8] = { - {"sensor", "temp", "htu", "temperature", jsonTempc, "", "", "°C"}, - {"sensor", "hum", "htu", "humidity", jsonHum, "", "", "%"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < HTUparametersCount; i++) { - //trc(HTUsensor[i][1]); - createDiscovery(HTUsensor[i][0], - HTUTOPIC, HTUsensor[i][1], (char*)getUniqueId(HTUsensor[i][1], HTUsensor[i][2]).c_str(), - will_Topic, HTUsensor[i][3], HTUsensor[i][4], - HTUsensor[i][5], HTUsensor[i][6], HTUsensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorLM75 - Log.trace(F("LM75Discovery" CR)); - char* LM75sensor[8] = {"sensor", "temp", "htu", "temperature", jsonTempc, "", "", "°C"}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - createDiscovery(LM75sensor[0], - LM75TOPIC, LM75sensor[1], (char*)getUniqueId(LM75sensor[1], LM75sensor[2]).c_str(), - will_Topic, LM75sensor[3], LM75sensor[4], - LM75sensor[5], LM75sensor[6], LM75sensor[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); -# endif - -# ifdef ZsensorAHTx0 -# include "config_AHTx0.h" -# define AHTparametersCount 2 - Log.trace(F("AHTx0Discovery" CR)); - char* AHTsensor[AHTparametersCount][8] = { - {"sensor", "temp", "aht", "temperature", jsonTempc, "", "", "°C"}, - {"sensor", "hum", "aht", "humidity", jsonHum, "", "", "%"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < AHTparametersCount; i++) { - createDiscovery(AHTsensor[i][0], - AHTTOPIC, AHTsensor[i][1], (char*)getUniqueId(AHTsensor[i][1], AHTsensor[i][2]).c_str(), - will_Topic, AHTsensor[i][3], AHTsensor[i][4], - AHTsensor[i][5], AHTsensor[i][6], AHTsensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorDHT -# include "config_DHT.h" -# define DHTparametersCount 2 - Log.trace(F("DHTDiscovery" CR)); - char* DHTsensor[DHTparametersCount][8] = { - {"sensor", "temp", "dht", "temperature", jsonTempc, "", "", "°C"}, - {"sensor", "hum", "dht", "humidity", jsonHum, "", "", "%"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < DHTparametersCount; i++) { - //trc(DHTsensor[i][1]); - createDiscovery(DHTsensor[i][0], - DHTTOPIC, DHTsensor[i][1], (char*)getUniqueId(DHTsensor[i][1], DHTsensor[i][2]).c_str(), - will_Topic, DHTsensor[i][3], DHTsensor[i][4], - DHTsensor[i][5], DHTsensor[i][6], DHTsensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorADC -# include "config_ADC.h" - - Log.trace(F("ADCDiscovery" CR)); - char* ADCsensor[8] = {"sensor", "adc", "", "", jsonAdc, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(ADCsensor[1]); - createDiscovery(ADCsensor[0], - ADCTOPIC, ADCsensor[1], (char*)getUniqueId(ADCsensor[1], ADCsensor[2]).c_str(), - will_Topic, ADCsensor[3], ADCsensor[4], - ADCsensor[5], ADCsensor[6], ADCsensor[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZsensorBH1750 -# include "config_BH1750.h" -# define BH1750parametersCount 3 - Log.trace(F("BH1750Discovery" CR)); - char* BH1750sensor[BH1750parametersCount][8] = { - {"sensor", "lux", "BH1750", "illuminance", jsonLux, "", "", "lx"}, - {"sensor", "ftCd", "BH1750", "irradiance", jsonFtcd, "", "", ""}, - {"sensor", "wattsm2", "BH1750", "irradiance", jsonWm2, "", "", "wm²"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < BH1750parametersCount; i++) { - //trc(BH1750sensor[i][1]); - createDiscovery(BH1750sensor[i][0], - subjectBH1750toMQTT, BH1750sensor[i][1], (char*)getUniqueId(BH1750sensor[i][1], BH1750sensor[i][2]).c_str(), - will_Topic, BH1750sensor[i][3], BH1750sensor[i][4], - BH1750sensor[i][5], BH1750sensor[i][6], BH1750sensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorMQ2 -# include "config_MQ2.h" -# define MQ2parametersCount 2 - Log.trace(F("MQ2Discovery" CR)); - char* MQ2sensor[MQ2parametersCount][8] = { - {"sensor", "gas", "MQ2", "gas", jsonVal, "", "", "ppm"}, - {"binary_sensor", "MQ2", "", "gas", jsonPresence, "true", "false", ""} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < MQ2parametersCount; i++) { - createDiscovery(MQ2sensor[i][0], - subjectMQ2toMQTT, MQ2sensor[i][1], (char*)getUniqueId(MQ2sensor[i][1], MQ2sensor[i][2]).c_str(), - will_Topic, MQ2sensor[i][3], MQ2sensor[i][4], - MQ2sensor[i][5], MQ2sensor[i][6], MQ2sensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - } -# endif - -# ifdef ZsensorTEMT6000 -# include "config_TEMT6000.h" -# define TEMT6000parametersCount 3 - Log.trace(F("TEMT6000Discovery" CR)); - char* TEMT6000sensor[TEMT6000parametersCount][8] = { - {"sensor", "lux", "TEMT6000", "illuminance", jsonLux, "", "", "lx"}, - {"sensor", "ftcd", "TEMT6000", "irradiance", jsonFtcd, "", "", ""}, - {"sensor", "wattsm2", "TEMT6000", "irradiance", jsonWm2, "", "", "wm²"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < TEMT6000parametersCount; i++) { - //trc(TEMT6000sensor[i][1]); - createDiscovery(TEMT6000sensor[i][0], - subjectTEMT6000toMQTT, TEMT6000sensor[i][1], (char*)getUniqueId(TEMT6000sensor[i][1], TEMT6000sensor[i][2]).c_str(), - will_Topic, TEMT6000sensor[i][3], TEMT6000sensor[i][4], - TEMT6000sensor[i][5], TEMT6000sensor[i][6], TEMT6000sensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorTSL2561 -# include "config_TSL2561.h" -# define TSL2561parametersCount 3 - Log.trace(F("TSL2561Discovery" CR)); - char* TSL2561sensor[TSL2561parametersCount][8] = { - {"sensor", "lux", "TSL2561", "illuminance", jsonLux, "", "", "lx"}, - {"sensor", "ftcd", "TSL2561", "irradiance", jsonFtcd, "", "", ""}, - {"sensor", "wattsm2", "TSL2561", "irradiance", jsonWm2, "", "", "wm²"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < TSL2561parametersCount; i++) { - //trc(TSL2561sensor[i][1]); - createDiscovery(TSL2561sensor[i][0], - subjectTSL12561toMQTT, TSL2561sensor[i][1], (char*)getUniqueId(TSL2561sensor[i][1], TSL2561sensor[i][2]).c_str(), - will_Topic, TSL2561sensor[i][3], TSL2561sensor[i][4], - TSL2561sensor[i][5], TSL2561sensor[i][6], TSL2561sensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorHCSR501 -# include "config_HCSR501.h" - Log.trace(F("HCSR501Discovery" CR)); - char* HCSR501sensor[8] = {"binary_sensor", "hcsr501", "", "motion", jsonPresence, "true", "false", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(HCSR501sensor[1]); - createDiscovery(HCSR501sensor[0], - subjectHCSR501toMQTT, HCSR501sensor[1], (char*)getUniqueId(HCSR501sensor[1], HCSR501sensor[2]).c_str(), - will_Topic, HCSR501sensor[3], HCSR501sensor[4], - HCSR501sensor[5], HCSR501sensor[6], HCSR501sensor[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZsensorGPIOInput -# include "config_GPIOInput.h" - Log.trace(F("GPIOInputDiscovery" CR)); - char* GPIOInputsensor[8] = {"binary_sensor", "GPIOInput", "", "", jsonGpio, INPUT_GPIO_ON_VALUE, INPUT_GPIO_OFF_VALUE, ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(GPIOInputsensor[1]); - createDiscovery(GPIOInputsensor[0], - subjectGPIOInputtoMQTT, GPIOInputsensor[1], (char*)getUniqueId(GPIOInputsensor[1], GPIOInputsensor[2]).c_str(), - will_Topic, GPIOInputsensor[3], GPIOInputsensor[4], - GPIOInputsensor[5], GPIOInputsensor[6], GPIOInputsensor[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZsensorINA226 -# include "config_INA226.h" -# define INA226parametersCount 3 - Log.trace(F("INA226Discovery" CR)); - char* INA226sensor[INA226parametersCount][8] = { - {"sensor", "volt", "INA226", "voltage", jsonVolt, "", "", "V"}, - {"sensor", "current", "INA226", "current", jsonCurrent, "", "", "A"}, - {"sensor", "power", "INA226", "power", jsonPower, "", "", "W"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < INA226parametersCount; i++) { - //trc(INA226sensor[i][1]); - createDiscovery(INA226sensor[i][0], - subjectINA226toMQTT, INA226sensor[i][1], (char*)getUniqueId(INA226sensor[i][1], INA226sensor[i][2]).c_str(), - will_Topic, INA226sensor[i][3], INA226sensor[i][4], - INA226sensor[i][5], INA226sensor[i][6], INA226sensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorDS1820 - extern void pubOneWire_HADiscovery(); - // Publish any DS1820 sensors found on the OneWire bus - pubOneWire_HADiscovery(); -# endif - -# ifdef ZactuatorONOFF -# include "config_ONOFF.h" - Log.trace(F("actuatorONOFFDiscovery" CR)); - char* actuatorONOFF[8] = {"switch", "actuatorONOFF", "", "", "{{ value_json.cmd }}", "{\"cmd\":1}", "{\"cmd\":0}", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(actuatorONOFF[1]); - createDiscovery(actuatorONOFF[0], - subjectGTWONOFFtoMQTT, actuatorONOFF[1], (char*)getUniqueId(actuatorONOFF[1], actuatorONOFF[2]).c_str(), - will_Topic, actuatorONOFF[3], actuatorONOFF[4], - actuatorONOFF[5], actuatorONOFF[6], actuatorONOFF[7], - 0, Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoONOFF, - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone, //State Class - "0", "1" //state_off, state_on - ); -# endif - -# ifdef ZsensorRN8209 -# include "config_RN8209.h" -# define RN8209parametersCount 4 - Log.trace(F("RN8209Discovery" CR)); - char* RN8209sensor[RN8209parametersCount][8] = { - {"sensor", "volt", "RN8209", "voltage", jsonVolt, "", "", "V"}, - {"sensor", "current", "RN8209", "current", jsonCurrent, "", "", "A"}, - {"sensor", "power", "RN8209", "power", jsonPower, "", "", "W"}, - {"binary_sensor", "inUse", "RN8209", "power", jsonInuseRN8209, "on", "off", ""} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < RN8209parametersCount; i++) { - String name = "NRG: " + String(RN8209sensor[i][1]); - createDiscovery(RN8209sensor[i][0], - subjectRN8209toMQTT, (char*)name.c_str(), (char*)getUniqueId(RN8209sensor[i][1], RN8209sensor[i][2]).c_str(), //set state_topic,name,uniqueId - will_Topic, RN8209sensor[i][3], RN8209sensor[i][4], //set availability_topic,device_class,value_template, - RN8209sensor[i][5], RN8209sensor[i][6], RN8209sensor[i][7], //set,payload_on,payload_off,unit_of_meas - 0, Gateway_AnnouncementMsg, will_Message, true, "", //set off_delay,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -// in addition to the MQTT Device Discovery -# if defined(ZgatewayRF) && defined(RF_on_HAS_as_MQTTSensor) - // Sensor to display RF received value - Log.trace(F("gatewayRFDiscovery" CR)); - char* gatewayRF[8] = {"sensor", "gatewayRF", "", "", jsonVal, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewayRF[1]); - createDiscovery(gatewayRF[0], -# if valueAsATopic - subjectRFtoMQTTvalueAsATopic, -# else - subjectRFtoMQTT, -# endif - gatewayRF[1], (char*)getUniqueId(gatewayRF[1], gatewayRF[2]).c_str(), - will_Topic, gatewayRF[3], gatewayRF[4], - gatewayRF[5], gatewayRF[6], gatewayRF[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - -# endif - -# ifdef ZgatewayRF2 -# include "config_RF.h" - - // Sensor to display RF received value - Log.trace(F("gatewayRF2Discovery" CR)); - char* gatewayRF2[8] = {"sensor", "gatewayRF2", "", "", jsonAddress, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewayRF2[1]); - createDiscovery(gatewayRF2[0], -# if valueAsATopic - subjectRF2toMQTTvalueAsATopic, -# else - subjectRF2toMQTT, -# endif - gatewayRF2[1], (char*)getUniqueId(gatewayRF2[1], gatewayRF2[2]).c_str(), - will_Topic, gatewayRF2[3], gatewayRF2[4], - gatewayRF2[5], gatewayRF2[6], gatewayRF2[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZgatewayRFM69 -# include "config_RFM69.h" - // Sensor to display RF received value - Log.trace(F("gatewayRFM69Discovery" CR)); - char* gatewayRFM69[8] = {"sensor", "gatewayRFM69", "", "", jsonVal, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewayRFM69[1]); - createDiscovery(gatewayRFM69[0], - subjectRFM69toMQTT, gatewayRFM69[1], (char*)getUniqueId(gatewayRFM69[1], gatewayRFM69[2]).c_str(), - will_Topic, gatewayRFM69[3], gatewayRFM69[4], - gatewayRFM69[5], gatewayRFM69[6], gatewayRFM69[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZgatewayLORA -# include "config_LORA.h" - // Sensor to display RF received value - Log.trace(F("gatewayLORADiscovery" CR)); - char* gatewayLORA[8] = {"sensor", "gatewayLORA", "", "", jsonMsg, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewayLORA[1]); - createDiscovery(gatewayLORA[0], - subjectLORAtoMQTT, gatewayLORA[1], (char*)getUniqueId(gatewayLORA[1], gatewayLORA[2]).c_str(), - will_Topic, gatewayLORA[3], gatewayLORA[4], - gatewayLORA[5], gatewayLORA[6], gatewayLORA[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - - createDiscovery("switch", //set Type - subjectLORAtoMQTT, "LORA: CRC", (char*)getUniqueId("enablecrc", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.enablecrc }}", //set availability_topic,device_class,value_template, - "{\"enablecrc\":true,\"save\":true}", "{\"enablecrc\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoLORAset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - - createDiscovery("switch", //set Type - subjectLORAtoMQTT, "LORA: Invert IQ", (char*)getUniqueId("invertiq", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.invertiq }}", //set availability_topic,device_class,value_template, - "{\"invertiq\":true,\"save\":true}", "{\"invertiq\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoLORAset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - - createDiscovery("switch", //set Type - subjectLORAtoMQTT, "LORA: Only Known", (char*)getUniqueId("onlyknown", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.onlyknown }}", //set availability_topic,device_class,value_template, - "{\"onlyknown\":true,\"save\":true}", "{\"onlyknown\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoLORAset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); -# endif - -# ifdef ZgatewaySRFB -# include "config_SRFB.h" - // Sensor to display RF received value - Log.trace(F("gatewaySRFBDiscovery" CR)); - char* gatewaySRFB[8] = {"sensor", "gatewaySRFB", "", "", jsonVal, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewaySRFB[1]); - createDiscovery(gatewaySRFB[0], - subjectSRFBtoMQTT, gatewaySRFB[1], (char*)getUniqueId(gatewaySRFB[1], gatewaySRFB[2]).c_str(), - will_Topic, gatewaySRFB[3], gatewaySRFB[4], - gatewaySRFB[5], gatewaySRFB[6], gatewaySRFB[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZgatewayPilight -# include "config_RF.h" - - // Sensor to display RF received value - Log.trace(F("gatewayPilightDiscovery" CR)); - char* gatewayPilight[8] = {"sensor", "gatewayPilight", "", "", jsonMsg, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewayPilight[1]); - createDiscovery(gatewayPilight[0], -# if valueAsATopic - subjectPilighttoMQTTvalueAsATopic, -# else - subjectPilighttoMQTT, -# endif - gatewayPilight[1], (char*)getUniqueId(gatewayPilight[1], gatewayPilight[2]).c_str(), - will_Topic, gatewayPilight[3], gatewayPilight[4], - gatewayPilight[5], gatewayPilight[6], gatewayPilight[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZgatewayIR -# include "config_IR.h" - // Sensor to display IR received value - Log.trace(F("gatewayIRDiscovery" CR)); - char* gatewayIR[8] = {"sensor", "gatewayIR", "", "", jsonVal, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewayIR[1]); - createDiscovery(gatewayIR[0], - subjectIRtoMQTT, gatewayIR[1], (char*)getUniqueId(gatewayIR[1], gatewayIR[2]).c_str(), - will_Topic, gatewayIR[3], gatewayIR[4], - gatewayIR[5], gatewayIR[6], gatewayIR[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef Zgateway2G -# include "config_2G.h" - // Sensor to display 2G received value - Log.trace(F("gateway2GDiscovery" CR)); - char* gateway2G[8] = {"sensor", "gateway2G", "", "", jsonMsg, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gateway2G[1]); - createDiscovery(gateway2G[0], - subject2GtoMQTT, gateway2G[1], (char*)getUniqueId(gateway2G[1], gateway2G[2]).c_str(), - will_Topic, gateway2G[3], gateway2G[4], - gateway2G[5], gateway2G[6], gateway2G[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# if defined(ZgatewayBT) || defined(SecondaryModule) -# ifdef ESP32 - - createDiscovery("number", //set Type - subjectBTtoMQTT, "BT: Connect interval", (char*)getUniqueId("intervalcnct", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.intervalcnct/60000 }}", //set availability_topic,device_class,value_template, - "{\"intervalcnct\":{{value*60000}},\"save\":true}", "", "min", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("number", //set Type - subjectBTtoMQTT, "BT: Scan duration", (char*)getUniqueId("scanduration", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.scanduration/1000 }}", //set availability_topic,device_class,value_template, - "{\"scanduration\":{{value*1000}},\"save\":true}", "", "s", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("button", //set Type - will_Topic, "BT: Force scan", (char*)getUniqueId("force_scan", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "", //set availability_topic,device_class,value_template, - "{\"interval\":0}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("button", //set Type - will_Topic, "BT: Erase config", (char*)getUniqueId("erase_bt_config", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "", //set availability_topic,device_class,value_template, - "{\"erase\":true}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone //State Class - ); - createDiscovery("switch", //set Type - subjectBTtoMQTT, "BT: Publish only sensors", (char*)getUniqueId("only_sensors", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.onlysensors }}", //set availability_topic,device_class,value_template, - "{\"onlysensors\":true,\"save\":true}", "{\"onlysensors\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - createDiscovery("switch", //set Type - subjectBTtoMQTT, "BT: Adaptive scan", (char*)getUniqueId("adaptive_scan", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.adaptivescan }}", //set availability_topic,device_class,value_template, - "{\"adaptivescan\":true,\"save\":true}", "{\"adaptivescan\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - createDiscovery("switch", //set Type - subjectBTtoMQTT, "BT: Enabled", (char*)getUniqueId("enabled", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.enabled }}", //set availability_topic,device_class,value_template, - "{\"enabled\":true,\"save\":true}", "{\"enabled\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - -# define EntitiesCount 9 - const char* obsoleteEntities[EntitiesCount][2] = { - // Remove previously created entities for version < 1.4.0 - {"switch", "active_scan"}, // Replaced by adaptive scan - // Remove previously created entities for version < 1.3.0 - {"number", "scanbcnct"}, // Now a connect interval - // Remove previously created entities for version < 1.2.0 - {"switch", "restart"}, // Now a button - {"switch", "erase"}, // Now a button - {"switch", "force_scan"}, // Now a button - {"sensor", "interval"}, // Now a number - {"sensor", "scanbcnct"}, // Now a number - {"switch", "ohdiscovery"}, // Now a new key - {"switch", "discovery"}}; // Now a new key - - for (int i = 0; i < EntitiesCount; i++) { - eraseTopic(obsoleteEntities[i][0], (char*)getUniqueId(obsoleteEntities[i][1], "").c_str()); - } - - btScanParametersDiscovery(); - - btPresenceParametersDiscovery(); - - createDiscovery("switch", //set Type - subjectBTtoMQTT, "BT: Publish HASS presence", (char*)getUniqueId("hasspresence", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.hasspresence }}", //set availability_topic,device_class,value_template, - "{\"hasspresence\":true,\"save\":true}", "{\"hasspresence\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - createDiscovery("switch", //set Type - subjectBTtoMQTT, "BT: Publish Advertisement data", (char*)getUniqueId("pubadvdata", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.pubadvdata }}", //set availability_topic,device_class,value_template, - "{\"pubadvdata\":true,\"save\":true}", "{\"pubadvdata\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - createDiscovery("switch", //set Type - subjectBTtoMQTT, "BT: Connect to devices", (char*)getUniqueId("bleconnect", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.bleconnect }}", //set availability_topic,device_class,value_template, - "{\"bleconnect\":true,\"save\":true}", "{\"bleconnect\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); -# if DEFAULT_LOW_POWER_MODE != DEACTIVATED - createDiscovery("switch", //set Type - subjectSYStoMQTT, "SYS: Low Power Mode command", (char*)getUniqueId("powermode", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.powermode | bool }}", //set availability_topic,device_class,value_template, - "{\"powermode\":1,\"save\":true}", "{\"powermode\":0,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available,is a gateway entity, command topic - "", "", "", "", true, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); -# else - // Remove previously created switch for version < 1.4.0 - eraseTopic("switch", (char*)getUniqueId("powermode", "").c_str()); -# endif -# endif -# endif -} -#else -void pubMqttDiscovery() {} -#endif +/* + OpenMQTTGateway Addon - ESP8266 or Arduino program for home automation + + Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker + Send and receiving command by MQTT + + This is the Home Assistant MQTT Discovery addon. + + Copyright: (c) Rafal Herok + + This file is part of OpenMQTTGateway. + + OpenMQTTGateway is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenMQTTGateway is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "User_config.h" + +#ifdef ZmqttDiscovery +# include "TheengsCommon.h" + +# ifdef ESP8266 +# include +# elif defined(ESP32) +# include + +# include "esp_mac.h" +# endif +# ifdef ESP32_ETHERNET +# include +# endif +# include "config_mqttDiscovery.h" + +extern bool ethConnected; +extern JsonArray modules; + +char discovery_prefix[parameters_size + 1] = discovery_Prefix; +// From https://github.com/home-assistant/core/blob/d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf/homeassistant/const.py#L225 +// List of classes available in Home Assistant +const char* availableHASSClasses[] = {"battery_charging", + "battery", + "carbon_dioxide", + "carbon_monoxide", + "connectivity", + "current", + "data_size", + "distance", + "door", + "duration", + "energy", + "enum", + "frequency", + "gas", + "humidity", + "illuminance", + "irradiance", + "lock", + "motion", + "moving", + "occupancy", + "pm1", + "pm10", + "pm25", + "power_factor", + "power", + "precipitation_intensity", + "precipitation", + "pressure", + "problem", + "restart", + "signal_strength", + "sound_pressure", + "temperature", + "timestamp", + "voltage", + "water", + "weight", + "wind_speed", + "window"}; + +// From https://github.com/home-assistant/core/blob/d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf/homeassistant/const.py#L379 +// List of units available in Home Assistant +const char* availableHASSUnits[] = {"A", + "B", + "UV index", + "V", + "W", + "W", + "bpm", + "bar", + "cm", + "dB", + "dBm", + "ft", + "h", + "hPa", + "Hz", + "kg", + "kW", + "kWh", + "km/h", + "lb", + "lx", + "m/s", + "m/s²", + "m³", + "mg/m³", + "min", + "mm", + "mm/h", + "ms", + "mV", + "µS/cm", + "μg/m³", + "Ω", + "%", + "°", + "°C", + "°F", + "s", + "wb²" +}; + +String getMacAddress() { + uint8_t baseMac[6]; + char baseMacChr[13] = {0}; +# if defined(ESP8266) + WiFi.macAddress(baseMac); + sprintf(baseMacChr, "%02X%02X%02X%02X%02X%02X", baseMac[0], baseMac[1], baseMac[2], baseMac[3], baseMac[4], baseMac[5]); +# elif defined(ESP32) + esp_read_mac(baseMac, ESP_MAC_WIFI_STA); + sprintf(baseMacChr, "%02X%02X%02X%02X%02X%02X", baseMac[0], baseMac[1], baseMac[2], baseMac[3], baseMac[4], baseMac[5]); +# else + sprintf(baseMacChr, "%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +# endif + return String(baseMacChr); +} + +String getUniqueId(String name, String sufix) { + String uniqueId = (String)getMacAddress() + "-" + name + sufix; + return String(uniqueId); +} + +# if defined(ZgatewayBT) || defined(SecondaryModule) +# include "config_BT.h" +/** + * Create a discover messages form a list of attribute + * + * @param mac the MAC address + * @param sensorList[][0] = component type + * @param sensorList[][1] = name + * @param sensorList[][2] = availability topic + * @param sensorList[][3] = device class + * @param sensorList[][4] = value template + * @param sensorList[][5] = payload on + * @param sensorList[][6] = payload off + * @param sensorList[][7] = unit of measurement + * @param sensorList[][8] = unit of measurement + * @param sensorCount number of sensor + * @param device_name name of sensors + * @param device_manufacturer name of manufacturer + * @param device_model the model + * */ +void createDiscoveryFromList(const char* mac, + const char* sensorList[][9], + int sensorCount, + const char* device_name, + const char* device_manufacturer, + const char* device_model) { + for (int i = 0; i < sensorCount; i++) { + String discovery_topic = String(subjectBTtoMQTT) + "/" + String(mac); + String unique_id = String(mac) + "-" + sensorList[i][1]; + + createDiscovery(sensorList[i][0], + discovery_topic.c_str(), sensorList[i][1], unique_id.c_str(), + will_Topic, sensorList[i][3], sensorList[i][4], + sensorList[i][5], sensorList[i][6], sensorList[i][7], + 0, "", "", false, "", + device_name, device_manufacturer, device_model, mac, false, + sensorList[i][8] //The state class + ); + } +} +# endif + +/** + * @brief Announce that the Gateway have the ability to raise Trigger. + * This function provide the configuration of the MQTT Device trigger ( @see https://www.home-assistant.io/integrations/device_trigger.mqtt/ ). + * All messages published by this function will be interpreted as configuration messages of Gateway Triggers. + * Instead, all messages published on the "triggerTopic" will be interpreted as Gateway trigger. + * + * @param triggerTopic Mandatory - The MQTT topic subscribed to receive trigger events. + * @param type The type of the trigger, e.g. button_short_press. Entries supported by the HA Frontend: button_short_press, button_short_release, button_long_press, button_long_release, button_double_press, button_triple_press, button_quadruple_press, button_quintuple_press. If set to an unsupported value, will render as subtype type, e.g. button_1 spammed with type set to spammed and subtype set to button_1 + * @param subtype The subtype of the trigger, e.g. button_1. Entries supported by the HA frontend: turn_on, turn_off, button_1, button_2, button_3, button_4, button_5, button_6. If set to an unsupported value, will render as subtype type, e.g. left_button pressed with type set to button_short_press and subtype set to left_button + * @param object_id The object_id of the trigger. + * @param value_template The template to render the value of the trigger. The template can use the variables trigger.id, trigger.type, trigger.subtype, trigger.payload, trigger.payload_json, trigger.topic, trigger.timestamp, trigger.value, trigger.value_json. The template can be a string or a JSON object. If the template is a JSON object, it must be a valid JSON object. If the template is a string, it will be rendered as a string. If the template is a JSON object, it will be rendered as a JSON object. + */ +void announceGatewayTrigger(const char* triggerTopic, + const char* type, + const char* subtype, + const char* object_id, + const char* value_template) { + //Create The Json + StaticJsonDocument jsonBuffer; + JsonObject sensor = jsonBuffer.to(); + + /** + * The type of automation, must be ‘trigger’. + * @see https://www.home-assistant.io/integrations/device_trigger.mqtt/#automation_type + */ + sensor["automation_type"] = "trigger"; + + /** + * Must be device_automation. Only allowed and required in MQTT auto discovery device messages. + * @see https://www.home-assistant.io/integrations/device_trigger.mqtt/#platform + * @see https://www.home-assistant.io/integrations/mqtt/#device-discovery-payload + */ + sensor["platform "] = "device_automation"; + + // The MQTT topic subscribed to receive trigger events. + if (triggerTopic && triggerTopic[0]) { + char state_topic[mqtt_topic_max_size]; + + strcpy(state_topic, mqtt_topic); + strcat(state_topic, gateway_name); + strcat(state_topic, triggerTopic); + + /** + * "info_topic" is not a standard field, for the message is required the filed "topic", but this filed is reserved and it is used to know where to publish the topic. + * If we want to send on the message the topic information is usefull to use this "info_topic" that will be not delete by the send function but converted to "topic" + */ + sensor["info_topic"] = state_topic; + } else { + Log.error(F("[RF] Error: topic is mandatory for device trigger Discovery" CR)); + return; + } + + /** + * The type of the trigger, e.g. button_short_press. + * Entries supported by the HA frontend: button_short_press, button_short_release, button_long_press, button_long_release, button_double_press, button_triple_press, button_quadruple_press, button_quintuple_press. + * If set to an unsupported value, will render as subtype type, e.g. button_1 spammed with type set to spammed and subtype set to button_1 + */ + if (type && type[0] != 0) { + sensor["type"] = type; + } else { + sensor["type"] = "button_short_press"; + } + + /** + * The subtype of the trigger, e.g. turn_on. + * Entries supported by the frontend: turn_on, turn_off, button_1, button_2, button_3, button_4, button_5, button_6. + * If set to an unsupported value, will render as subtype type, e.g. left_button pressed with type set to button_short_press and subtype set to left_button + */ + if (subtype && subtype[0] != 0) { + sensor["subtype"] = subtype; + } else { + sensor["subtype"] = "turn_on"; + } + + // ------------------ START DEVICE DECLARATION -------------------------------------------------- + // TODO: This section, like the almost identical one in createDiscovery, should be placed in a + // separate function and managed specifically to avoid errors in representing the device + // in the HASS world. + // ------------------------------------------------------------------------------------------------- + + // Information about the device: this device trigger is a part of to tie it into the HA device registry. + StaticJsonDocument jsonDeviceBuffer; + JsonObject device = jsonDeviceBuffer.to(); + + // A link to the webpage that can manage the configuration of this device. + if (ethConnected) { +# ifdef ESP32_ETHERNET + device["configuration_url"] = String("http://") + String(ETH.localIP().toString()) + String("/"); //configuration_url +# endif + } else { + device["configuration_url"] = String("http://") + String(WiFi.localIP().toString()) + String("/"); //configuration_url + } + + /* + * A list of connections of the device to the outside world as a list of tuples [connection_type, connection_identifier]. + * For example the MAC address of a network interface: "connections": [["mac", "02:5b:26:a8:dc:12"]]. + */ + JsonArray connections = device.createNestedArray("connections"); + JsonArray connection_mac = connections.createNestedArray(); + connection_mac.add("mac"); + connection_mac.add(getMacAddress()); + + // A list of IDs that uniquely identify the device. For example a serial number. + String unique_id = String(getMacAddress()); + JsonArray identifiers = device.createNestedArray("identifiers"); + identifiers.add(unique_id); + + // The manufacturer of the device. + device["mf"] = GATEWAY_MANUFACTURER; + + // The model of the device. +# ifndef GATEWAY_MODEL + String model = ""; + serializeJson(modules, model); + device["mdl"] = model; +# else + device["mdl"] = GATEWAY_MODEL; +# endif + + // The name of the device. + device["name"] = String(gateway_name); + device["sw"] = OMG_VERSION; + // ------------------ END DEVICE DECLARATION ------------------ // + + sensor["device"] = device; //device representing the board + + if (value_template && value_template[0]) { + sensor["value_template"] = String(value_template); + } + + /* Publish on the topic + The discovery topic needs to be: /device_automation/[/]/config. + + Note that only one trigger may be defined per unique discovery topic. + Also note that the combination of type and subtype should be unique for a device. + + */ + + String topic_to_publish = String(discovery_prefix) + "/device_automation/" + String(unique_id) + "/" + object_id + "/config"; + Log.trace(F("Announce Gatewy Trigger %s" CR), topic_to_publish.c_str()); + sensor["topic"] = topic_to_publish; + sensor["retain"] = true; + enqueueJsonObject(sensor); +} + +/* + * Remove a substring p from a given string s +*/ +std::string remove_substring(std::string s, const std::string& p) { + std::string::size_type n = p.length(); + + for (std::string::size_type i = s.find(p); + i != std::string::npos; + i = s.find(p)) + s.erase(i, n); + + return s; +} + +/** + * @brief Generate message and publish it on an MQTT discovery explorer. For HA @see https://www.home-assistant.io/docs/mqtt/discovery/ + * + * @param sensor_type the Type + * @param st_topic set state topic, + * @param s_name set name, + * @param unique_id set uniqueId + * @param availability_topic set availability_topic, + * @param device_class set device_class, + * @param value_template set value_template, + * @param payload_on set payload_on, + * @param payload_off set payload_off, + * @param unit_of_meas set unit_of_meas, + * @param off_delay set off_delay + * @param payload_available set payload_available, + * @param payload_not_available set payload_not_available + * @param gateway_entity set is a gateway entity, + * @param cmd_topic set command topic + * @param device_name set device name, + * @param device_manufacturer set device manufacturer, + * @param device_model set device model, + * @param device_id set device(BLE)/entity(RTL_433) identification, + * @param retainCmd set retain + * @param state_class set state class + * + * */ +void createDiscovery(const char* sensor_type, + const char* st_topic, const char* s_name, const char* unique_id, + const char* availability_topic, const char* device_class, const char* value_template, + const char* payload_on, const char* payload_off, const char* unit_of_meas, + int off_delay, + const char* payload_available, const char* payload_not_available, bool gateway_entity, const char* cmd_topic, + const char* device_name, const char* device_manufacturer, const char* device_model, const char* device_id, bool retainCmd, + const char* state_class, const char* state_off, const char* state_on, const char* enum_options, const char* command_template) { + StaticJsonDocument jsonBuffer; + JsonObject sensor = jsonBuffer.to(); + + // If a component cannot render it's state (f.i. KAKU relays) no state topic + // should be added. Without a state topic HA will use optimistic mode for the + // component by default. The Home Assistant UI for optimistic switches + // (separate on and off icons) allows for multiple + // subsequent on commands. This is required for dimming on KAKU relays like + // the ACM-300. + if (st_topic && st_topic[0]) { + char state_topic[mqtt_topic_max_size]; + // If not an entity belonging to the gateway we put wild card for the location and gateway name + // allowing to have the entity detected by several gateways and a consistent discovery topic among the gateways + if (gateway_entity) { + strcpy(state_topic, mqtt_topic); + strcat(state_topic, gateway_name); + } else { + strcpy(state_topic, "+/+"); + } + strcat(state_topic, st_topic); + if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { + sensor["tilt_status_t"] = state_topic; // tilt_status_topic for blind + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { + sensor["pos_t"] = state_topic; // position_topic for curtain + } else { + sensor["stat_t"] = state_topic; + } + } + + if (availability_topic && availability_topic[0] && gateway_entity) { + char avty_topic[mqtt_topic_max_size]; + strcpy(avty_topic, mqtt_topic); + strcat(avty_topic, gateway_name); + strcat(avty_topic, availability_topic); + sensor["avty_t"] = avty_topic; + } + + if (device_class && device_class[0]) { + // We check if the class belongs to HAAS classes list + int num_classes = sizeof(availableHASSClasses) / sizeof(availableHASSClasses[0]); + for (int i = 0; i < num_classes; i++) { // see class list and size into config_mqttDiscovery.h + if (strcmp(availableHASSClasses[i], device_class) == 0) { + sensor["dev_cla"] = device_class; //device_class + } + } + } + + if (unit_of_meas && unit_of_meas[0]) { + // We check if the class belongs to HAAS units list + int num_units = sizeof(availableHASSUnits) / sizeof(availableHASSUnits[0]); + for (int i = 0; i < num_units; i++) { // see units list and size into config_mqttDiscovery.h + if (strcmp(availableHASSUnits[i], unit_of_meas) == 0) { + sensor["unit_of_meas"] = unit_of_meas; //unit_of_measurement*/ + } + } + } + sensor["name"] = s_name; //name + sensor["uniq_id"] = unique_id; //unique_id + sensor["obj_id"] = unique_id; // object_id aka entity_id default value as unique_id + if (retainCmd) + sensor["retain"] = retainCmd; // Retain command + if (value_template && value_template[0]) { + if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { + sensor["tilt_status_tpl"] = value_template; // tilt_status_template for blind + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { + sensor["pos_tpl"] = value_template; // position_template for curtain + } else { + sensor["val_tpl"] = value_template; //HA Auto discovery + } + } + if (payload_on && payload_on[0]) { + if (strcmp(sensor_type, "button") == 0) { + sensor["pl_prs"] = payload_on; // payload_press for a button press + } else if (strcmp(sensor_type, "number") == 0) { + sensor["cmd_tpl"] = payload_on; // payload_on for a switch + } else if (strcmp(sensor_type, "update") == 0) { + sensor["pl_inst"] = payload_on; // payload_install for update + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { + int value = std::stoi(payload_on); + sensor["tilt_opnd_val"] = value; // tilt_open_value for blind + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { + int value = std::stoi(payload_on); + sensor["pos_open"] = value; // open value for curtain + } else { + if (strcmp(payload_on, "True") == 0 || strcmp(payload_on, "true") == 0) { + sensor["pl_on"] = true; + } else { + sensor["pl_on"] = payload_on; // payload_on for the rest + } + } + } + if (payload_off && payload_off[0]) { + if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { + sensor["pl_cls"] = payload_off; // payload_close for cover + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { + int value = std::stoi(payload_off); + sensor["pos_clsd"] = value; // closed value for curtain + } else { + if (strcmp(payload_off, "False") == 0 || strcmp(payload_off, "false") == 0) { + sensor["pl_off"] = false; + } else { + sensor["pl_off"] = payload_off; //payload_off for the rest + } + } + } + if (command_template && command_template[0]) { + if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { + sensor["tilt_cmd_tpl"] = command_template; //command_template + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { + sensor["set_pos_tpl"] = command_template; //command_template + } else { + sensor["cmd_tpl"] = command_template; //command_template + } + } + if (strcmp(sensor_type, "device_tracker") == 0) + sensor["source_type"] = "bluetooth_le"; // payload_install for update + if (off_delay != 0) + sensor["off_delay"] = off_delay; //off_delay + if (payload_available[0]) + sensor["pl_avail"] = payload_available; // payload_on + if (payload_not_available[0]) + sensor["pl_not_avail"] = payload_not_available; //payload_off + if (state_class && state_class[0]) + sensor["stat_cla"] = state_class; //add the state class on the sensors ( https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes ) + if (state_on != nullptr) + if (strcmp(state_on, "true") == 0) { + sensor["stat_on"] = true; + } else { + sensor["stat_on"] = state_on; + } + if (state_off != nullptr) + if (strcmp(state_off, "false") == 0) { + sensor["stat_off"] = false; + } else { + sensor["stat_off"] = state_off; + } + if (cmd_topic[0]) { + char command_topic[mqtt_topic_max_size]; + strcpy(command_topic, mqtt_topic); + strcat(command_topic, gateway_name); + strcat(command_topic, cmd_topic); + if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { + sensor["tilt_cmd_t"] = command_topic; // tilt_command_topic for cover + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { + sensor["set_pos_t"] = command_topic; // position_command_topic for curtain + } else { + sensor["cmd_t"] = command_topic; //command_topic + } + } + + if (enum_options != nullptr) { + sensor["options"] = enum_options; + } + + StaticJsonDocument jsonDeviceBuffer; + JsonObject device = jsonDeviceBuffer.to(); + JsonArray identifiers = device.createNestedArray("ids"); + + if (gateway_entity) { + //device representing the board + device["name"] = String(gateway_name); +# ifndef GATEWAY_MODEL + String model = ""; + serializeJson(modules, model); + device["mdl"] = model; +# else + device["mdl"] = GATEWAY_MODEL; +# endif + device["mf"] = GATEWAY_MANUFACTURER; + if (ethConnected) { +# ifdef ESP32_ETHERNET + device["cu"] = String("http://") + String(ETH.localIP().toString()) + String("/"); //configuration_url +# endif + } else { + device["cu"] = String("http://") + String(WiFi.localIP().toString()) + String("/"); //configuration_url + } + + device["sw"] = OMG_VERSION; + identifiers.add(String(getMacAddress())); + } else { + //The Connections + if (device_id[0]) { + JsonArray connections = device.createNestedArray("cns"); + JsonArray connection_mac = connections.createNestedArray(); + connection_mac.add("mac"); + connection_mac.add(device_id); + //Device representing the actual sensor/switch device + //The Device ID + identifiers.add(device_id); + } + + if (device_manufacturer[0]) { + device["mf"] = device_manufacturer; + } + + if (device_model[0]) { + device["mdl"] = device_model; + } + + // generate unique device name by adding the second half of the device_id only if device_name and device_id are different and we don't want to use the BLE name + if (device_name[0]) { + #if defined(ZgatewayBT) // displayDeviceName only applies when running Bluetooth + if (strcmp(device_id, device_name) != 0 && device_id[0] && !displayDeviceName) { + #else !ForceDeviceName // Support ForceDeviceName for esp8266's + if (strcmp(device_id, device_name) != 0 && device_id[0] && !ForceDeviceName) { + #endif + device["name"] = device_name + String("-") + String(device_id + 6); + } else { + device["name"] = device_name; + } + } + + device["via_device"] = String(getMacAddress()); //mac address of the gateway so that the devices link to the gateway + } + + sensor["device"] = device; + + String topic = String(discovery_prefix) + "/" + String(sensor_type) + "/" + String(unique_id) + "/config"; + Log.trace(F("Announce Device %s on %s" CR), String(sensor_type).c_str(), topic.c_str()); + sensor["topic"] = topic; + sensor["retain"] = true; + enqueueJsonObject(sensor); +} + +void eraseTopic(const char* sensor_type, const char* unique_id) { + if (sensor_type == NULL || unique_id == NULL) { + return; + } + String topic = String(discovery_prefix) + "/" + String(sensor_type) + "/" + String(unique_id) + "/config"; + Log.trace(F("Erase entity discovery %s on %s" CR), String(sensor_type).c_str(), topic.c_str()); + pubMQTT((char*)topic.c_str(), "", true); +} + +# if defined(ZgatewayBT) || defined(SecondaryModule) +void btPresenceParametersDiscovery() { + createDiscovery("number", //set Type + subjectBTtoMQTT, "BT: Presence/Tracker timeout", (char*)getUniqueId("presenceawaytimer", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.presenceawaytimer/60000 }}", //set availability_topic,device_class,value_template, + "{\"presenceawaytimer\":{{value*60000}},\"save\":true}", "", "min", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, + stateClassNone //State Class + ); +} +void btScanParametersDiscovery() { + createDiscovery("number", //set Type + subjectBTtoMQTT, "BT: Interval between scans", (char*)getUniqueId("interval", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.interval/1000 }}", //set availability_topic,device_class,value_template, + "{\"interval\":{{value*1000}},\"save\":true}", "", "s", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, + stateClassNone //State Class + ); + createDiscovery("number", //set Type + subjectBTtoMQTT, "BT: Interval between active scans", (char*)getUniqueId("intervalacts", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.intervalacts/1000 }}", //set availability_topic,device_class,value_template, + "{\"intervalacts\":{{value*1000}},\"save\":true}", "", "s", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, + stateClassNone //State Class + ); +} +# endif + +void pubMqttDiscovery() { + Log.trace(F("omgStatusDiscovery" CR)); +# ifdef SecondaryModule + String uptimeName = "SYS: Uptime " + String(SecondaryModule); + String uptimeId = "uptime-" + String(SecondaryModule); + createDiscovery("sensor", //set Type + subjectSYStoMQTTSecondaryModule, uptimeName.c_str(), (char*)getUniqueId(uptimeId, "").c_str(), //set state_topic,name,uniqueId + will_Topic, "duration", "{{ value_json.uptime }}", //set availability_topic,device_class,value_template, + "", "", "s", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + String freememName = "SYS: Free memory " + String(SecondaryModule); + String freememId = "freemem-" + String(SecondaryModule); + createDiscovery("sensor", //set Type + subjectSYStoMQTTSecondaryModule, freememName.c_str(), (char*)getUniqueId(freememId, "").c_str(), //set state_topic,name,uniqueId + will_Topic, "data_size", "{{ value_json.freemem }}", //set availability_topic,device_class,value_template, + "", "", "B", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + String restartName = "SYS: Restart " + String(SecondaryModule); + String restartId = "restart-" + String(SecondaryModule); + createDiscovery("button", //set Type + will_Topic, restartName.c_str(), (char*)getUniqueId(restartId, "").c_str(), //set state_topic,name,uniqueId + will_Topic, "restart", "", //set availability_topic,device_class,value_template, + "{\"cmd\":\"restart\"}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSsetSecondaryModule, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + createDiscovery("binary_sensor", //set Type + will_Topic, "SYS: Connectivity", (char*)getUniqueId("connectivity", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "connectivity", "", //set availability_topic,device_class,value_template, + Gateway_AnnouncementMsg, will_Message, "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Uptime", (char*)getUniqueId("uptime", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "duration", "{{ value_json.uptime }}", //set availability_topic,device_class,value_template, + "", "", "s", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Free memory", (char*)getUniqueId("freemem", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "data_size", "{{ value_json.freemem }}", //set availability_topic,device_class,value_template, + "", "", "B", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: IP", (char*)getUniqueId("ip", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.ip }}", //set availability_topic,device_class,value_template, + "", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("switch", //set Type + subjectSYStoMQTT, "SYS: Auto discovery", (char*)getUniqueId("disc", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.disc }}", //set availability_topic,device_class,value_template, + "{\"disc\":true,\"save\":true}", "{\"disc\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_avalaible,payload_not avalaible ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain, + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); +# ifdef LED_ADDRESSABLE + createDiscovery("number", //set Type + subjectSYStoMQTT, "SYS: LED Brightness", (char*)getUniqueId("rgbb", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ (value_json.rgbb/2.55) | round(0) }}", //set availability_topic,device_class,value_template, + "{\"rgbb\":{{ (value*2.55) | round(0) }},\"save\":true}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, + stateClassNone //State Class + ); +# endif + +# ifdef ZdisplaySSD1306 +# include "config_SSD1306.h" + createDiscovery("switch", //set Type + subjectSSD1306toMQTT, "SSD1306: Control", (char*)getUniqueId("onstate", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.onstate }}", //set availability_topic,device_class,value_template, + "{\"onstate\":true,\"save\":true}", "{\"onstate\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSSD1306set, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + createDiscovery("switch", //set Type + subjectWebUItoMQTT, "SSD1306: Display metric", (char*)getUniqueId("displayMetric", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.displayMetric }}", //set availability_topic,device_class,value_template, + "{\"displayMetric\":true,\"save\":true}", "{\"displayMetric\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoWebUIset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + createDiscovery("number", //set Type + subjectSSD1306toMQTT, "SSD1306: Brightness", (char*)getUniqueId("brightness", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.brightness }}", //set availability_topic,device_class,value_template, + "{\"brightness\":{{value}},\"save\":true}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSSD1306set, //set,payload_available,payload_not available,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifndef ESP32_ETHERNET + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: RSSI", (char*)getUniqueId("rssi", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "signal_strength", "{{ value_json.rssi }}", //set availability_topic,device_class,value_template, + "", "", "dB", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif +# if defined(ESP32) && !defined(NO_INT_TEMP_READING) + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Internal temperature", (char*)getUniqueId("tempc", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "temperature", "{{ value_json.tempc | round(1)}}", //set availability_topic,device_class,value_template, + "", "", "°C", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_avalaible,payload_not avalaible ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC + stateClassMeasurement //State Class + ); +# if defined(ZboardM5STICKC) || defined(ZboardM5STICKCP) || defined(ZboardM5TOUGH) + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Bat voltage", (char*)getUniqueId("m5batvoltage", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "voltage", "{{ value_json.m5batvoltage }}", //set availability_topic,device_class,value_template, + "", "", "V", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Bat current", (char*)getUniqueId("m5batcurrent", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "current", "{{ value_json.m5batcurrent }}", //set availability_topic,device_class,value_template, + "", "", "A", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Vin voltage", (char*)getUniqueId("m5vinvoltage", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "voltage", "{{ value_json.m5vinvoltage }}", //set availability_topic,device_class,value_template, + "", "", "V", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Vin current", (char*)getUniqueId("m5vincurrent", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "current", "{{ value_json.m5vincurrent }}", //set availability_topic,device_class,value_template, + "", "", "A", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif +# ifdef ZboardM5STACK + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Batt level", (char*)getUniqueId("m5battlevel", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "battery", "{{ value_json.m5battlevel }}", //set availability_topic,device_class,value_template, + "", "", "%", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("binary_sensor", //set Type + subjectSYStoMQTT, "SYS: Is Charging", (char*)getUniqueId("m5ischarging", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "{{ value_json.m5ischarging }}", "", //set availability_topic,device_class,value_template, + "", "", "%", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("binary_sensor", //set Type + subjectSYStoMQTT, "SYS: Is Charge Full", (char*)getUniqueId("m5ischargefull", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "{{ value_json.m5ischargefull }}", "", //set availability_topic,device_class,value_template, + "", "", "%", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif +# endif + createDiscovery("button", //set Type + will_Topic, "SYS: Restart gateway", (char*)getUniqueId("restart", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "restart", "", //set availability_topic,device_class,value_template, + "{\"cmd\":\"restart\"}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("button", //set Type + will_Topic, "SYS: Erase credentials", (char*)getUniqueId("erase", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "", //set availability_topic,device_class,value_template, + "{\"cmd\":\"erase\"}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# ifdef MQTT_HTTPS_FW_UPDATE + createDiscovery("update", //set Type + subjectRLStoMQTT, "SYS: Firmware Update", (char*)getUniqueId("update", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "firmware", "", //set availability_topic,device_class,value_template, + LATEST_OR_DEV, "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSupdate, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZsensorBME280 +# include "config_BME280.h" +# define BMEparametersCount 5 + Log.trace(F("bme280Discovery" CR)); + char* BMEsensor[BMEparametersCount][8] = { + {"sensor", "temp", "bme", "temperature", jsonTempc, "", "", "°C"}, + {"sensor", "pa", "bme", "pressure", jsonPa, "", "", "hPa"}, + {"sensor", "hum", "bme", "humidity", jsonHum, "", "", "%"}, + {"sensor", "altim", "bme", "", jsonAltim, "", "", "m"}, + {"sensor", "altift", "bme", "", jsonAltif, "", "", "ft"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < BMEparametersCount; i++) { + createDiscovery(BMEsensor[i][0], + BMETOPIC, BMEsensor[i][1], (char*)getUniqueId(BMEsensor[i][1], BMEsensor[i][2]).c_str(), + will_Topic, BMEsensor[i][3], BMEsensor[i][4], + BMEsensor[i][5], BMEsensor[i][6], BMEsensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + } +# endif + +# ifdef ZsensorHTU21 +# include "config_HTU21.h" +# define HTUparametersCount 2 + Log.trace(F("htu21Discovery" CR)); + char* HTUsensor[HTUparametersCount][8] = { + {"sensor", "temp", "htu", "temperature", jsonTempc, "", "", "°C"}, + {"sensor", "hum", "htu", "humidity", jsonHum, "", "", "%"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < HTUparametersCount; i++) { + //trc(HTUsensor[i][1]); + createDiscovery(HTUsensor[i][0], + HTUTOPIC, HTUsensor[i][1], (char*)getUniqueId(HTUsensor[i][1], HTUsensor[i][2]).c_str(), + will_Topic, HTUsensor[i][3], HTUsensor[i][4], + HTUsensor[i][5], HTUsensor[i][6], HTUsensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorLM75 + Log.trace(F("LM75Discovery" CR)); + char* LM75sensor[8] = {"sensor", "temp", "htu", "temperature", jsonTempc, "", "", "°C"}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + createDiscovery(LM75sensor[0], + LM75TOPIC, LM75sensor[1], (char*)getUniqueId(LM75sensor[1], LM75sensor[2]).c_str(), + will_Topic, LM75sensor[3], LM75sensor[4], + LM75sensor[5], LM75sensor[6], LM75sensor[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); +# endif + +# ifdef ZsensorAHTx0 +# include "config_AHTx0.h" +# define AHTparametersCount 2 + Log.trace(F("AHTx0Discovery" CR)); + char* AHTsensor[AHTparametersCount][8] = { + {"sensor", "temp", "aht", "temperature", jsonTempc, "", "", "°C"}, + {"sensor", "hum", "aht", "humidity", jsonHum, "", "", "%"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < AHTparametersCount; i++) { + createDiscovery(AHTsensor[i][0], + AHTTOPIC, AHTsensor[i][1], (char*)getUniqueId(AHTsensor[i][1], AHTsensor[i][2]).c_str(), + will_Topic, AHTsensor[i][3], AHTsensor[i][4], + AHTsensor[i][5], AHTsensor[i][6], AHTsensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorDHT +# include "config_DHT.h" +# define DHTparametersCount 2 + Log.trace(F("DHTDiscovery" CR)); + char* DHTsensor[DHTparametersCount][8] = { + {"sensor", "temp", "dht", "temperature", jsonTempc, "", "", "°C"}, + {"sensor", "hum", "dht", "humidity", jsonHum, "", "", "%"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < DHTparametersCount; i++) { + //trc(DHTsensor[i][1]); + createDiscovery(DHTsensor[i][0], + DHTTOPIC, DHTsensor[i][1], (char*)getUniqueId(DHTsensor[i][1], DHTsensor[i][2]).c_str(), + will_Topic, DHTsensor[i][3], DHTsensor[i][4], + DHTsensor[i][5], DHTsensor[i][6], DHTsensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorADC +# include "config_ADC.h" + + Log.trace(F("ADCDiscovery" CR)); + char* ADCsensor[8] = {"sensor", "adc", "", "", jsonAdc, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(ADCsensor[1]); + createDiscovery(ADCsensor[0], + ADCTOPIC, ADCsensor[1], (char*)getUniqueId(ADCsensor[1], ADCsensor[2]).c_str(), + will_Topic, ADCsensor[3], ADCsensor[4], + ADCsensor[5], ADCsensor[6], ADCsensor[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZsensorBH1750 +# include "config_BH1750.h" +# define BH1750parametersCount 3 + Log.trace(F("BH1750Discovery" CR)); + char* BH1750sensor[BH1750parametersCount][8] = { + {"sensor", "lux", "BH1750", "illuminance", jsonLux, "", "", "lx"}, + {"sensor", "ftCd", "BH1750", "irradiance", jsonFtcd, "", "", ""}, + {"sensor", "wattsm2", "BH1750", "irradiance", jsonWm2, "", "", "wm²"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < BH1750parametersCount; i++) { + //trc(BH1750sensor[i][1]); + createDiscovery(BH1750sensor[i][0], + subjectBH1750toMQTT, BH1750sensor[i][1], (char*)getUniqueId(BH1750sensor[i][1], BH1750sensor[i][2]).c_str(), + will_Topic, BH1750sensor[i][3], BH1750sensor[i][4], + BH1750sensor[i][5], BH1750sensor[i][6], BH1750sensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorMQ2 +# include "config_MQ2.h" +# define MQ2parametersCount 2 + Log.trace(F("MQ2Discovery" CR)); + char* MQ2sensor[MQ2parametersCount][8] = { + {"sensor", "gas", "MQ2", "gas", jsonVal, "", "", "ppm"}, + {"binary_sensor", "MQ2", "", "gas", jsonPresence, "true", "false", ""} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < MQ2parametersCount; i++) { + createDiscovery(MQ2sensor[i][0], + subjectMQ2toMQTT, MQ2sensor[i][1], (char*)getUniqueId(MQ2sensor[i][1], MQ2sensor[i][2]).c_str(), + will_Topic, MQ2sensor[i][3], MQ2sensor[i][4], + MQ2sensor[i][5], MQ2sensor[i][6], MQ2sensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + } +# endif + +# ifdef ZsensorTEMT6000 +# include "config_TEMT6000.h" +# define TEMT6000parametersCount 3 + Log.trace(F("TEMT6000Discovery" CR)); + char* TEMT6000sensor[TEMT6000parametersCount][8] = { + {"sensor", "lux", "TEMT6000", "illuminance", jsonLux, "", "", "lx"}, + {"sensor", "ftcd", "TEMT6000", "irradiance", jsonFtcd, "", "", ""}, + {"sensor", "wattsm2", "TEMT6000", "irradiance", jsonWm2, "", "", "wm²"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < TEMT6000parametersCount; i++) { + //trc(TEMT6000sensor[i][1]); + createDiscovery(TEMT6000sensor[i][0], + subjectTEMT6000toMQTT, TEMT6000sensor[i][1], (char*)getUniqueId(TEMT6000sensor[i][1], TEMT6000sensor[i][2]).c_str(), + will_Topic, TEMT6000sensor[i][3], TEMT6000sensor[i][4], + TEMT6000sensor[i][5], TEMT6000sensor[i][6], TEMT6000sensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorTSL2561 +# include "config_TSL2561.h" +# define TSL2561parametersCount 3 + Log.trace(F("TSL2561Discovery" CR)); + char* TSL2561sensor[TSL2561parametersCount][8] = { + {"sensor", "lux", "TSL2561", "illuminance", jsonLux, "", "", "lx"}, + {"sensor", "ftcd", "TSL2561", "irradiance", jsonFtcd, "", "", ""}, + {"sensor", "wattsm2", "TSL2561", "irradiance", jsonWm2, "", "", "wm²"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < TSL2561parametersCount; i++) { + //trc(TSL2561sensor[i][1]); + createDiscovery(TSL2561sensor[i][0], + subjectTSL12561toMQTT, TSL2561sensor[i][1], (char*)getUniqueId(TSL2561sensor[i][1], TSL2561sensor[i][2]).c_str(), + will_Topic, TSL2561sensor[i][3], TSL2561sensor[i][4], + TSL2561sensor[i][5], TSL2561sensor[i][6], TSL2561sensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorHCSR501 +# include "config_HCSR501.h" + Log.trace(F("HCSR501Discovery" CR)); + char* HCSR501sensor[8] = {"binary_sensor", "hcsr501", "", "motion", jsonPresence, "true", "false", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(HCSR501sensor[1]); + createDiscovery(HCSR501sensor[0], + subjectHCSR501toMQTT, HCSR501sensor[1], (char*)getUniqueId(HCSR501sensor[1], HCSR501sensor[2]).c_str(), + will_Topic, HCSR501sensor[3], HCSR501sensor[4], + HCSR501sensor[5], HCSR501sensor[6], HCSR501sensor[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZsensorGPIOInput +# include "config_GPIOInput.h" + Log.trace(F("GPIOInputDiscovery" CR)); + char* GPIOInputsensor[8] = {"binary_sensor", "GPIOInput", "", "", jsonGpio, INPUT_GPIO_ON_VALUE, INPUT_GPIO_OFF_VALUE, ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(GPIOInputsensor[1]); + createDiscovery(GPIOInputsensor[0], + subjectGPIOInputtoMQTT, GPIOInputsensor[1], (char*)getUniqueId(GPIOInputsensor[1], GPIOInputsensor[2]).c_str(), + will_Topic, GPIOInputsensor[3], GPIOInputsensor[4], + GPIOInputsensor[5], GPIOInputsensor[6], GPIOInputsensor[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZsensorINA226 +# include "config_INA226.h" +# define INA226parametersCount 3 + Log.trace(F("INA226Discovery" CR)); + char* INA226sensor[INA226parametersCount][8] = { + {"sensor", "volt", "INA226", "voltage", jsonVolt, "", "", "V"}, + {"sensor", "current", "INA226", "current", jsonCurrent, "", "", "A"}, + {"sensor", "power", "INA226", "power", jsonPower, "", "", "W"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < INA226parametersCount; i++) { + //trc(INA226sensor[i][1]); + createDiscovery(INA226sensor[i][0], + subjectINA226toMQTT, INA226sensor[i][1], (char*)getUniqueId(INA226sensor[i][1], INA226sensor[i][2]).c_str(), + will_Topic, INA226sensor[i][3], INA226sensor[i][4], + INA226sensor[i][5], INA226sensor[i][6], INA226sensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorDS1820 + extern void pubOneWire_HADiscovery(); + // Publish any DS1820 sensors found on the OneWire bus + pubOneWire_HADiscovery(); +# endif + +# ifdef ZactuatorONOFF +# include "config_ONOFF.h" + Log.trace(F("actuatorONOFFDiscovery" CR)); + char* actuatorONOFF[8] = {"switch", "actuatorONOFF", "", "", "{{ value_json.cmd }}", "{\"cmd\":1}", "{\"cmd\":0}", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(actuatorONOFF[1]); + createDiscovery(actuatorONOFF[0], + subjectGTWONOFFtoMQTT, actuatorONOFF[1], (char*)getUniqueId(actuatorONOFF[1], actuatorONOFF[2]).c_str(), + will_Topic, actuatorONOFF[3], actuatorONOFF[4], + actuatorONOFF[5], actuatorONOFF[6], actuatorONOFF[7], + 0, Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoONOFF, + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone, //State Class + "0", "1" //state_off, state_on + ); +# endif + +# ifdef ZsensorRN8209 +# include "config_RN8209.h" +# define RN8209parametersCount 4 + Log.trace(F("RN8209Discovery" CR)); + char* RN8209sensor[RN8209parametersCount][8] = { + {"sensor", "volt", "RN8209", "voltage", jsonVolt, "", "", "V"}, + {"sensor", "current", "RN8209", "current", jsonCurrent, "", "", "A"}, + {"sensor", "power", "RN8209", "power", jsonPower, "", "", "W"}, + {"binary_sensor", "inUse", "RN8209", "power", jsonInuseRN8209, "on", "off", ""} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < RN8209parametersCount; i++) { + String name = "NRG: " + String(RN8209sensor[i][1]); + createDiscovery(RN8209sensor[i][0], + subjectRN8209toMQTT, (char*)name.c_str(), (char*)getUniqueId(RN8209sensor[i][1], RN8209sensor[i][2]).c_str(), //set state_topic,name,uniqueId + will_Topic, RN8209sensor[i][3], RN8209sensor[i][4], //set availability_topic,device_class,value_template, + RN8209sensor[i][5], RN8209sensor[i][6], RN8209sensor[i][7], //set,payload_on,payload_off,unit_of_meas + 0, Gateway_AnnouncementMsg, will_Message, true, "", //set off_delay,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +// in addition to the MQTT Device Discovery +# if defined(ZgatewayRF) && defined(RF_on_HAS_as_MQTTSensor) + // Sensor to display RF received value + Log.trace(F("gatewayRFDiscovery" CR)); + char* gatewayRF[8] = {"sensor", "gatewayRF", "", "", jsonVal, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewayRF[1]); + createDiscovery(gatewayRF[0], +# if valueAsATopic + subjectRFtoMQTTvalueAsATopic, +# else + subjectRFtoMQTT, +# endif + gatewayRF[1], (char*)getUniqueId(gatewayRF[1], gatewayRF[2]).c_str(), + will_Topic, gatewayRF[3], gatewayRF[4], + gatewayRF[5], gatewayRF[6], gatewayRF[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + +# endif + +# ifdef ZgatewayRF2 +# include "config_RF.h" + + // Sensor to display RF received value + Log.trace(F("gatewayRF2Discovery" CR)); + char* gatewayRF2[8] = {"sensor", "gatewayRF2", "", "", jsonAddress, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewayRF2[1]); + createDiscovery(gatewayRF2[0], +# if valueAsATopic + subjectRF2toMQTTvalueAsATopic, +# else + subjectRF2toMQTT, +# endif + gatewayRF2[1], (char*)getUniqueId(gatewayRF2[1], gatewayRF2[2]).c_str(), + will_Topic, gatewayRF2[3], gatewayRF2[4], + gatewayRF2[5], gatewayRF2[6], gatewayRF2[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZgatewayRFM69 +# include "config_RFM69.h" + // Sensor to display RF received value + Log.trace(F("gatewayRFM69Discovery" CR)); + char* gatewayRFM69[8] = {"sensor", "gatewayRFM69", "", "", jsonVal, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewayRFM69[1]); + createDiscovery(gatewayRFM69[0], + subjectRFM69toMQTT, gatewayRFM69[1], (char*)getUniqueId(gatewayRFM69[1], gatewayRFM69[2]).c_str(), + will_Topic, gatewayRFM69[3], gatewayRFM69[4], + gatewayRFM69[5], gatewayRFM69[6], gatewayRFM69[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZgatewayLORA +# include "config_LORA.h" + // Sensor to display RF received value + Log.trace(F("gatewayLORADiscovery" CR)); + char* gatewayLORA[8] = {"sensor", "gatewayLORA", "", "", jsonMsg, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewayLORA[1]); + createDiscovery(gatewayLORA[0], + subjectLORAtoMQTT, gatewayLORA[1], (char*)getUniqueId(gatewayLORA[1], gatewayLORA[2]).c_str(), + will_Topic, gatewayLORA[3], gatewayLORA[4], + gatewayLORA[5], gatewayLORA[6], gatewayLORA[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + + createDiscovery("switch", //set Type + subjectLORAtoMQTT, "LORA: CRC", (char*)getUniqueId("enablecrc", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.enablecrc }}", //set availability_topic,device_class,value_template, + "{\"enablecrc\":true,\"save\":true}", "{\"enablecrc\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoLORAset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + + createDiscovery("switch", //set Type + subjectLORAtoMQTT, "LORA: Invert IQ", (char*)getUniqueId("invertiq", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.invertiq }}", //set availability_topic,device_class,value_template, + "{\"invertiq\":true,\"save\":true}", "{\"invertiq\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoLORAset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + + createDiscovery("switch", //set Type + subjectLORAtoMQTT, "LORA: Only Known", (char*)getUniqueId("onlyknown", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.onlyknown }}", //set availability_topic,device_class,value_template, + "{\"onlyknown\":true,\"save\":true}", "{\"onlyknown\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoLORAset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); +# endif + +# ifdef ZgatewaySRFB +# include "config_SRFB.h" + // Sensor to display RF received value + Log.trace(F("gatewaySRFBDiscovery" CR)); + char* gatewaySRFB[8] = {"sensor", "gatewaySRFB", "", "", jsonVal, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewaySRFB[1]); + createDiscovery(gatewaySRFB[0], + subjectSRFBtoMQTT, gatewaySRFB[1], (char*)getUniqueId(gatewaySRFB[1], gatewaySRFB[2]).c_str(), + will_Topic, gatewaySRFB[3], gatewaySRFB[4], + gatewaySRFB[5], gatewaySRFB[6], gatewaySRFB[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZgatewayPilight +# include "config_RF.h" + + // Sensor to display RF received value + Log.trace(F("gatewayPilightDiscovery" CR)); + char* gatewayPilight[8] = {"sensor", "gatewayPilight", "", "", jsonMsg, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewayPilight[1]); + createDiscovery(gatewayPilight[0], +# if valueAsATopic + subjectPilighttoMQTTvalueAsATopic, +# else + subjectPilighttoMQTT, +# endif + gatewayPilight[1], (char*)getUniqueId(gatewayPilight[1], gatewayPilight[2]).c_str(), + will_Topic, gatewayPilight[3], gatewayPilight[4], + gatewayPilight[5], gatewayPilight[6], gatewayPilight[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZgatewayIR +# include "config_IR.h" + // Sensor to display IR received value + Log.trace(F("gatewayIRDiscovery" CR)); + char* gatewayIR[8] = {"sensor", "gatewayIR", "", "", jsonVal, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewayIR[1]); + createDiscovery(gatewayIR[0], + subjectIRtoMQTT, gatewayIR[1], (char*)getUniqueId(gatewayIR[1], gatewayIR[2]).c_str(), + will_Topic, gatewayIR[3], gatewayIR[4], + gatewayIR[5], gatewayIR[6], gatewayIR[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef Zgateway2G +# include "config_2G.h" + // Sensor to display 2G received value + Log.trace(F("gateway2GDiscovery" CR)); + char* gateway2G[8] = {"sensor", "gateway2G", "", "", jsonMsg, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gateway2G[1]); + createDiscovery(gateway2G[0], + subject2GtoMQTT, gateway2G[1], (char*)getUniqueId(gateway2G[1], gateway2G[2]).c_str(), + will_Topic, gateway2G[3], gateway2G[4], + gateway2G[5], gateway2G[6], gateway2G[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# if defined(ZgatewayBT) || defined(SecondaryModule) +# ifdef ESP32 + + createDiscovery("number", //set Type + subjectBTtoMQTT, "BT: Connect interval", (char*)getUniqueId("intervalcnct", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.intervalcnct/60000 }}", //set availability_topic,device_class,value_template, + "{\"intervalcnct\":{{value*60000}},\"save\":true}", "", "min", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("number", //set Type + subjectBTtoMQTT, "BT: Scan duration", (char*)getUniqueId("scanduration", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.scanduration/1000 }}", //set availability_topic,device_class,value_template, + "{\"scanduration\":{{value*1000}},\"save\":true}", "", "s", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("button", //set Type + will_Topic, "BT: Force scan", (char*)getUniqueId("force_scan", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "", //set availability_topic,device_class,value_template, + "{\"interval\":0}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("button", //set Type + will_Topic, "BT: Erase config", (char*)getUniqueId("erase_bt_config", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "", //set availability_topic,device_class,value_template, + "{\"erase\":true}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone //State Class + ); + createDiscovery("switch", //set Type + subjectBTtoMQTT, "BT: Publish only sensors", (char*)getUniqueId("only_sensors", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.onlysensors }}", //set availability_topic,device_class,value_template, + "{\"onlysensors\":true,\"save\":true}", "{\"onlysensors\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + createDiscovery("switch", //set Type + subjectBTtoMQTT, "BT: Adaptive scan", (char*)getUniqueId("adaptive_scan", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.adaptivescan }}", //set availability_topic,device_class,value_template, + "{\"adaptivescan\":true,\"save\":true}", "{\"adaptivescan\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + createDiscovery("switch", //set Type + subjectBTtoMQTT, "BT: Enabled", (char*)getUniqueId("enabled", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.enabled }}", //set availability_topic,device_class,value_template, + "{\"enabled\":true,\"save\":true}", "{\"enabled\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + +# define EntitiesCount 9 + const char* obsoleteEntities[EntitiesCount][2] = { + // Remove previously created entities for version < 1.4.0 + {"switch", "active_scan"}, // Replaced by adaptive scan + // Remove previously created entities for version < 1.3.0 + {"number", "scanbcnct"}, // Now a connect interval + // Remove previously created entities for version < 1.2.0 + {"switch", "restart"}, // Now a button + {"switch", "erase"}, // Now a button + {"switch", "force_scan"}, // Now a button + {"sensor", "interval"}, // Now a number + {"sensor", "scanbcnct"}, // Now a number + {"switch", "ohdiscovery"}, // Now a new key + {"switch", "discovery"}}; // Now a new key + + for (int i = 0; i < EntitiesCount; i++) { + eraseTopic(obsoleteEntities[i][0], (char*)getUniqueId(obsoleteEntities[i][1], "").c_str()); + } + + btScanParametersDiscovery(); + + btPresenceParametersDiscovery(); + + createDiscovery("switch", //set Type + subjectBTtoMQTT, "BT: Publish HASS presence", (char*)getUniqueId("hasspresence", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.hasspresence }}", //set availability_topic,device_class,value_template, + "{\"hasspresence\":true,\"save\":true}", "{\"hasspresence\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + createDiscovery("switch", //set Type + subjectBTtoMQTT, "BT: Publish Advertisement data", (char*)getUniqueId("pubadvdata", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.pubadvdata }}", //set availability_topic,device_class,value_template, + "{\"pubadvdata\":true,\"save\":true}", "{\"pubadvdata\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + createDiscovery("switch", //set Type + subjectBTtoMQTT, "BT: Connect to devices", (char*)getUniqueId("bleconnect", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.bleconnect }}", //set availability_topic,device_class,value_template, + "{\"bleconnect\":true,\"save\":true}", "{\"bleconnect\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); +# if DEFAULT_LOW_POWER_MODE != DEACTIVATED + createDiscovery("switch", //set Type + subjectSYStoMQTT, "SYS: Low Power Mode command", (char*)getUniqueId("powermode", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.powermode | bool }}", //set availability_topic,device_class,value_template, + "{\"powermode\":1,\"save\":true}", "{\"powermode\":0,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available,is a gateway entity, command topic + "", "", "", "", true, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); +# else + // Remove previously created switch for version < 1.4.0 + eraseTopic("switch", (char*)getUniqueId("powermode", "").c_str()); +# endif +# endif +# endif +} +#else +void pubMqttDiscovery() {} +#endif From b350e73806738216134fec41b72a1937e718b106 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 10 Aug 2025 19:29:18 +1200 Subject: [PATCH 21/24] Revert format change --- main/gatewayBT.cpp | 3699 ++++++++++++++++++++-------------------- main/mqttDiscovery.cpp | 3131 +++++++++++++++++----------------- 2 files changed, 3396 insertions(+), 3434 deletions(-) diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index f310deeffa..77f98539d0 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -1,1866 +1,1833 @@ -/* - OpenMQTTGateway - ESP8266 or Arduino program for home automation - - Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker - Send and receiving command by MQTT - - This gateway enables to: - - publish MQTT data to a topic related to BLE devices data - - Copyright: (c)Florian ROBERT - - This file is part of OpenMQTTGateway. - - OpenMQTTGateway is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - OpenMQTTGateway is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Thanks to wolass https://github.com/wolass for suggesting me HM 10 and dinosd https://github.com/dinosd/BLE_PROXIMITY for inspiring me how to implement the gateway -*/ -#include "User_config.h" - -#ifdef ZgatewayBT -# include "TheengsCommon.h" - -SemaphoreHandle_t semaphoreCreateOrUpdateDevice; -SemaphoreHandle_t semaphoreBLEOperation; -QueueHandle_t BLEQueue; -unsigned long scanCount = 0; -# include -# include -# include -# include -# include -# include - -# include - -# include "TheengsCommon.h" -# include "config_mqttDiscovery.h" -# include "gatewayBLEConnect.h" -# include "soc/timer_group_reg.h" -# include "soc/timer_group_struct.h" - -using namespace std; - -// Global struct to store live BT configuration data -BTConfig_s BTConfig; - -# if BLEDecoder -# include -# if BLEDecryptor -# include "mbedtls/ccm.h" -# include "mbedtls/aes.h" -# endif -TheengsDecoder decoder; -# endif - -static TaskHandle_t xCoreTaskHandle; -static TaskHandle_t xProcBLETaskHandle; - -struct decompose { - int start; - int len; - bool reverse; -}; - -vector BLEactions; - -vector devices; -int newDevices = 0; - -static BLEdevice NO_BT_DEVICE_FOUND = { - {0}, - 0, - false, - false, - false, - false, - (char)UNKWNON_MODEL, - 0, -}; -static bool oneWhite = false; - -extern bool BTProcessLock; -extern int queueLength; - -void setupBTTasksAndBLE(); -bool checkIfIsTracker(char ch); -void hass_presence(JsonObject& HomePresence); -void BTforceScan(); - -void BTConfig_init() { - BTConfig.bleConnect = AttemptBLEConnect; - BTConfig.BLEinterval = TimeBtwRead; - BTConfig.adaptiveScan = AdaptiveBLEScan; - BTConfig.intervalActiveScan = TimeBtwActive; - BTConfig.intervalConnect = TimeBtwConnect; - BTConfig.scanDuration = Scan_duration; - BTConfig.pubOnlySensors = PublishOnlySensors; - BTConfig.pubRandomMACs = PublishRandomMACs; - BTConfig.presenceEnable = HassPresence; - BTConfig.presenceTopic = subjectHomePresence; - BTConfig.presenceUseBeaconUuid = useBeaconUuidForPresence; - BTConfig.minRssi = MinimumRSSI; - BTConfig.extDecoderEnable = UseExtDecoder; - BTConfig.extDecoderTopic = MQTTDecodeTopic; - BTConfig.filterConnectable = BLE_FILTER_CONNECTABLE; - BTConfig.pubAdvData = pubBLEAdvData; - BTConfig.pubBeaconUuidForTopic = useBeaconUuidForTopic; - BTConfig.ignoreWBlist = false; - BTConfig.presenceAwayTimer = PresenceAwayTimer; - BTConfig.movingTimer = MovingTimer; - BTConfig.forcePassiveScan = false; - BTConfig.enabled = EnableBT; -} - -unsigned long timeBetweenConnect = 0; -unsigned long timeBetweenActive = 0; - -String stateBTMeasures(bool start) { - StaticJsonDocument jsonBuffer; - JsonObject jo = jsonBuffer.to(); - jo["bleconnect"] = BTConfig.bleConnect; - jo["interval"] = BTConfig.BLEinterval; - jo["adaptivescan"] = BTConfig.adaptiveScan; - jo["intervalacts"] = BTConfig.intervalActiveScan; - jo["intervalcnct"] = BTConfig.intervalConnect; - jo["scanduration"] = BTConfig.scanDuration; - jo["hasspresence"] = BTConfig.presenceEnable; - jo["prestopic"] = BTConfig.presenceTopic; - jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; - jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value - jo["extDecoderEnable"] = BTConfig.extDecoderEnable; - jo["extDecoderTopic"] = BTConfig.extDecoderTopic; - jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; - jo["ignoreWBlist"] = BTConfig.ignoreWBlist; - jo["forcepscn"] = BTConfig.forcePassiveScan; - jo["tskstck"] = uxTaskGetStackHighWaterMark(xProcBLETaskHandle); - jo["crstck"] = uxTaskGetStackHighWaterMark(xCoreTaskHandle); - jo["enabled"] = BTConfig.enabled; - jo["scnct"] = scanCount; -# if BLEDecoder - jo["onlysensors"] = BTConfig.pubOnlySensors; - jo["randommacs"] = BTConfig.pubRandomMACs; - jo["filterConnectable"] = BTConfig.filterConnectable; - jo["pubadvdata"] = BTConfig.pubAdvData; - jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; - jo["movingtimer"] = BTConfig.movingTimer; -# endif - - if (start) { - Log.notice(F("BT sys: ")); - serializeJsonPretty(jsonBuffer, Serial); - Serial.println(); - return ""; // Do not try to erase/write/send config at startup - } - String output; - serializeJson(jo, output); - jo["origin"] = subjectBTtoMQTT; - enqueueJsonObject(jo, QueueSemaphoreTimeOutTask); - return (output); -} - -void BTConfig_fromJson(JsonObject& BTdata, bool startup = false) { - // Attempts to connect to eligible devices or not - Config_update(BTdata, "bleconnect", BTConfig.bleConnect); - // Identify AdaptiveScan deactivation to pass to continuous mode or activation to come back to default settings - if (startup == false) { - if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == false && BTConfig.presenceEnable == true) { - BTdata["adaptivescan"] = true; - } else if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == true && BTConfig.presenceEnable == false) { - BTdata["adaptivescan"] = false; - } - - if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == false && BTConfig.adaptiveScan == true) { - BTdata["interval"] = MinTimeBtwScan; - BTdata["intervalacts"] = MinTimeBtwScan; - BTdata["scanduration"] = MinScanDuration; - } else if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == true && BTConfig.adaptiveScan == false) { - BTdata["interval"] = TimeBtwRead; - BTdata["intervalacts"] = TimeBtwActive; - BTdata["scanduration"] = Scan_duration; - } - // Identify if the gateway is enabled or not and stop start accordingly - if (BTdata.containsKey("enabled") && BTdata["enabled"] == false && BTConfig.enabled == true) { - // Stop the gateway but without deinit to enable a future BT restart - stopProcessing(false); - } else if (BTdata.containsKey("enabled") && BTdata["enabled"] == true && BTConfig.enabled == false) { - BTProcessLock = false; - setupBTTasksAndBLE(); - } - } - // Home Assistant presence message - Config_update(BTdata, "hasspresence", BTConfig.presenceEnable); - // Time before before active scan - // Scan interval set - and avoid intervalacts to be lower than interval - if (BTdata.containsKey("interval") && BTdata["interval"] != 0) { - BTConfig.adaptiveScan = false; - Config_update(BTdata, "interval", BTConfig.BLEinterval); - if (BTConfig.intervalActiveScan < BTConfig.BLEinterval) { - Config_update(BTdata, "interval", BTConfig.intervalActiveScan); - } - } - // Define if the scan is adaptive or not - and avoid intervalacts to be lower than interval - if (BTdata.containsKey("intervalacts") && BTdata["intervalacts"] < BTConfig.BLEinterval) { - BTConfig.adaptiveScan = false; - // Config_update(BTdata, "interval", BTConfig.intervalActiveScan); - BTConfig.intervalActiveScan = BTConfig.BLEinterval; - } else { - Config_update(BTdata, "intervalacts", BTConfig.intervalActiveScan); - } - // Adaptive scan set - Config_update(BTdata, "adaptivescan", BTConfig.adaptiveScan); - // Time before a connect set - Config_update(BTdata, "intervalcnct", BTConfig.intervalConnect); - // publish all BLE devices discovered or only the identified sensors (like temperature sensors) - Config_update(BTdata, "scanduration", BTConfig.scanDuration); - // define the duration for a scan; in milliseconds - Config_update(BTdata, "onlysensors", BTConfig.pubOnlySensors); - // publish devices which randomly change their MAC addresses - Config_update(BTdata, "randommacs", BTConfig.pubRandomMACs); - // Home Assistant presence message topic - Config_update(BTdata, "prestopic", BTConfig.presenceTopic); - // Home Assistant presence message use iBeacon UUID - Config_update(BTdata, "presuseuuid", BTConfig.presenceUseBeaconUuid); - // Timer to trigger a device state as offline if not seen - Config_update(BTdata, "presenceawaytimer", BTConfig.presenceAwayTimer); - // Timer to trigger a device state as offline if not seen - Config_update(BTdata, "movingtimer", BTConfig.movingTimer); - // Force passive scan - Config_update(BTdata, "forcepscn", BTConfig.forcePassiveScan); - // MinRSSI set - Config_update(BTdata, "minrssi", BTConfig.minRssi); - // Send undecoded device data - Config_update(BTdata, "extDecoderEnable", BTConfig.extDecoderEnable); - // Topic to send undecoded device data - Config_update(BTdata, "extDecoderTopic", BTConfig.extDecoderTopic); - // Sets whether to filter publishing - Config_update(BTdata, "filterConnectable", BTConfig.filterConnectable); - // Publish advertisement data - Config_update(BTdata, "pubadvdata", BTConfig.pubAdvData); - // Use iBeacon UUID as topic, instead of sender (random) MAC address - Config_update(BTdata, "pubuuid4topic", BTConfig.pubBeaconUuidForTopic); - // Disable Whitelist & Blacklist - Config_update(BTdata, "ignoreWBlist", (BTConfig.ignoreWBlist)); - // Enable or disable the BT gateway - Config_update(BTdata, "enabled", BTConfig.enabled); - - stateBTMeasures(startup); - - if (BTdata.containsKey("erase") && BTdata["erase"].as()) { - // Erase config from NVS (non-volatile storage) - preferences.begin(Gateway_Short_Name, false); - if (preferences.isKey("BTConfig")) { - int result = preferences.remove("BTConfig"); - Log.notice(F("BT config erase result: %d" CR), result); - preferences.end(); - return; // Erase prevails on save, so skipping save - } else { - preferences.end(); - Log.notice(F("BT config not found" CR)); - } - } - - if (BTdata.containsKey("save") && BTdata["save"].as()) { - StaticJsonDocument jsonBuffer; - JsonObject jo = jsonBuffer.to(); - jo["bleconnect"] = BTConfig.bleConnect; - jo["interval"] = BTConfig.BLEinterval; - jo["adaptivescan"] = BTConfig.adaptiveScan; - jo["intervalacts"] = BTConfig.intervalActiveScan; - jo["intervalcnct"] = BTConfig.intervalConnect; - jo["scanduration"] = BTConfig.scanDuration; - jo["onlysensors"] = BTConfig.pubOnlySensors; - jo["randommacs"] = BTConfig.pubRandomMACs; - jo["hasspresence"] = BTConfig.presenceEnable; - jo["prestopic"] = BTConfig.presenceTopic; - jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; - jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value - jo["extDecoderEnable"] = BTConfig.extDecoderEnable; - jo["extDecoderTopic"] = BTConfig.extDecoderTopic; - jo["filterConnectable"] = BTConfig.filterConnectable; - jo["pubadvdata"] = BTConfig.pubAdvData; - jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; - jo["ignoreWBlist"] = BTConfig.ignoreWBlist; - jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; - jo["movingtimer"] = BTConfig.movingTimer; - jo["forcepscn"] = BTConfig.forcePassiveScan; - jo["enabled"] = BTConfig.enabled; - // Save config into NVS (non-volatile storage) - String conf = ""; - serializeJson(jsonBuffer, conf); - preferences.begin(Gateway_Short_Name, false); - int result = preferences.putString("BTConfig", conf); - preferences.end(); - Log.notice(F("BT config save: %s, result: %d" CR), conf.c_str(), result); - } -} - -void BTConfig_load() { - StaticJsonDocument jsonBuffer; - preferences.begin(Gateway_Short_Name, true); - if (preferences.isKey("BTConfig")) { - auto error = deserializeJson(jsonBuffer, preferences.getString("BTConfig", "{}")); - preferences.end(); - Log.notice(F("BT config loaded" CR)); - if (error) { - Log.error(F("BT config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity()); - return; - } - if (jsonBuffer.isNull()) { - Log.warning(F("BT config is null" CR)); - return; - } - JsonObject jo = jsonBuffer.as(); - BTConfig_fromJson(jo, true); // Never send MQTT message with config - Log.notice(F("BT config loaded" CR)); - } else { - preferences.end(); - Log.notice(F("BT config not found" CR)); - } -} - -void PublishDeviceData(JsonObject& BLEdata); - -atomic_int forceBTScan; - -void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type = 0, const char* name = ""); - -BLEdevice* getDeviceByMac(const char* mac); // Declared here to avoid pre-compilation issue (misplaced auto declaration by pio) -BLEdevice* getDeviceByMac(const char* mac) { - Log.trace(F("getDeviceByMac %s" CR), mac); - - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - if ((strcmp((*it)->macAdr, mac) == 0)) { - return *it; - } - } - return &NO_BT_DEVICE_FOUND; -} - -bool updateWorB(JsonObject& BTdata, bool isWhite) { - Log.trace(F("update WorB" CR)); - const char* jsonKey = isWhite ? "white-list" : "black-list"; - - int size = BTdata[jsonKey].size(); - if (size == 0) - return false; - - for (int i = 0; i < size; i++) { - const char* mac = BTdata[jsonKey][i]; - createOrUpdateDevice(mac, (isWhite ? device_flags_isWhiteL : device_flags_isBlackL), - UNKWNON_MODEL); - } - - return true; -} - -void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type, const char* name) { - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(30000)) == pdFALSE) { - Log.error(F("Semaphore NOT taken" CR)); - return; - } - BLEdevice* device = getDeviceByMac(mac); - if (device == &NO_BT_DEVICE_FOUND) { - Log.trace(F("add %s" CR), mac); - //new device - device = new BLEdevice(); - strcpy(device->macAdr, mac); - device->isDisc = flags & device_flags_isDisc; - device->isWhtL = flags & device_flags_isWhiteL; - device->isBlkL = flags & device_flags_isBlackL; - device->connect = flags & device_flags_connect; - device->macType = mac_type; - // Check name length - if (strlen(name) > 20) { - Log.warning(F("Name too long, truncating" CR)); - strncpy(device->name, name, 20); - device->name[19] = '\0'; - } else { - strcpy(device->name, name); - } - device->sensorModel_id = model; - device->lastUpdate = millis(); - devices.push_back(device); - newDevices++; - } else { - Log.trace(F("update %s" CR), mac); - device->lastUpdate = millis(); - device->macType = mac_type; - - if (flags & device_flags_isDisc) { - device->isDisc = true; - } - - if (flags & device_flags_connect) { - device->connect = true; - } - - if (model != UNKWNON_MODEL && device->sensorModel_id == UNKWNON_MODEL) { - newDevices++; - device->isDisc = false; - device->sensorModel_id = model; - } - - // If a device has been added to the white-list, flag it so it can be auto-detected - if (!device->isWhtL && flags & device_flags_isWhiteL) { - newDevices++; - } - if (flags & device_flags_isWhiteL || flags & device_flags_isBlackL) { - device->isWhtL = flags & device_flags_isWhiteL; - device->isBlkL = flags & device_flags_isBlackL; - } - } - - // update oneWhite flag - oneWhite = oneWhite || device->isWhtL; - - xSemaphoreGive(semaphoreCreateOrUpdateDevice); -} - -void updateDevicesStatus() { - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - BLEdevice* p = *it; - unsigned long now = millis(); - // Check for tracker status - bool isTracker = false; -# if BLEDecoder - std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); - if (tag.length() >= 4) { - isTracker = checkIfIsTracker(tag[3]); - } - // Device tracker devices - if (isTracker) { // We apply the offline status only for tracking device, can be extended further to all the devices - if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.presenceAwayTimer) && (now > BTConfig.presenceAwayTimer)) && - (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = p->macAdr; - BLEdata["state"] = "offline"; - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - // We set the lastUpdate to 0 to avoid replublishing the offline state - p->lastUpdate = 0; - } - } - // Moving detection devices (devices with an accelerometer) - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BC08) { - if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.movingTimer) && (now > BTConfig.movingTimer)) && - (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = p->macAdr; - BLEdata["state"] = "offline"; - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - // We set the lastUpdate to 0 to avoid replublishing the offline state - p->lastUpdate = 0; - } - } -# endif - } -} - -void dumpDevices() { -# if LOG_LEVEL > LOG_LEVEL_NOTICE - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - BLEdevice* p = *it; - Log.trace(F("macAdr %s" CR), p->macAdr); - Log.trace(F("macType %d" CR), p->macType); - Log.trace(F("isDisc %d" CR), p->isDisc); - Log.trace(F("isWhtL %d" CR), p->isWhtL); - Log.trace(F("isBlkL %d" CR), p->isBlkL); - Log.trace(F("connect %d" CR), p->connect); - Log.trace(F("sensorModel_id %d" CR), p->sensorModel_id); - Log.trace(F("LastUpdate %u" CR), p->lastUpdate); - } -# endif -} - -void strupp(char* beg) { - while ((*beg = toupper(*beg))) - ++beg; -} - -# ifdef ZmqttDiscovery -void DT24Discovery(const char* mac, const char* sensorModel_id) { -# define DT24parametersCount 7 - Log.trace(F("DT24Discovery" CR)); - const char* DT24sensor[DT24parametersCount][9] = { - {"sensor", "volt", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "amp", mac, "current", jsonCurrent, "", "", "A", stateClassMeasurement}, - {"sensor", "watt", mac, "power", jsonPower, "", "", "W", stateClassMeasurement}, - {"sensor", "watt-hour", mac, "power", jsonEnergy, "", "", "kWh", stateClassMeasurement}, - {"sensor", "price", mac, "", jsonMsg, "", "", "", stateClassNone}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"binary_sensor", "inUse", mac, "power", jsonInuse, "", "", "", stateClassNone} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, DT24sensor, DT24parametersCount, "DT24", "ATorch", sensorModel_id); -} - -void BM2Discovery(const char* mac, const char* sensorModel_id) { -# define BM2parametersCount 2 - Log.trace(F("BM2Discovery" CR)); - const char* BM2sensor[BM2parametersCount][9] = { - {"sensor", "volt", mac, "voltage", jsonVoltBM2, "", "", "V", stateClassMeasurement}, // We use a json definition that retrieve only data from the BM2 decoder, as this sensor also advertize volt as an iBeacon - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, BM2sensor, BM2parametersCount, "BM2", "Generic", sensorModel_id); -} - -void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) { -# define LYWSD03MMCparametersCount 4 - Log.trace(F("LYWSD03MMCDiscovery" CR)); - const char* LYWSD03MMCsensor[LYWSD03MMCparametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, LYWSD03MMCsensor, LYWSD03MMCparametersCount, "LYWSD03MMC", "Xiaomi", sensorModel); -} - -void MHO_C401Discovery(const char* mac, const char* sensorModel) { -# define MHO_C401parametersCount 4 - Log.trace(F("MHO_C401Discovery" CR)); - const char* MHO_C401sensor[MHO_C401parametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, MHO_C401sensor, MHO_C401parametersCount, "MHO_C401", "Xiaomi", sensorModel); -} - -void HHCCJCY01HHCCDiscovery(const char* mac, const char* sensorModel) { -# define HHCCJCY01HHCCparametersCount 5 - Log.trace(F("HHCCJCY01HHCCDiscovery" CR)); - const char* HHCCJCY01HHCCsensor[HHCCJCY01HHCCparametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "lux", mac, "illuminance", jsonLux, "", "", "lx", stateClassMeasurement}, - {"sensor", "fer", mac, "", jsonFer, "", "", "µS/cm", stateClassMeasurement}, - {"sensor", "moi", mac, "", jsonMoi, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, HHCCJCY01HHCCsensor, HHCCJCY01HHCCparametersCount, "HHCCJCY01HHCC", "Xiaomi", sensorModel); -} - -void XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel) { -# define XMWSDJ04MMCparametersCount 4 - Log.trace(F("XMWSDJ04MMCDiscovery" CR)); - const char* XMWSDJ04MMCsensor[XMWSDJ04MMCparametersCount][9] = { - {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, - {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - createDiscoveryFromList(mac, XMWSDJ04MMCsensor, XMWSDJ04MMCparametersCount, "XMWSDJ04MMC", "Xiaomi", sensorModel); -} - -void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) { - Log.trace(F("xxWSD0xMMCDiscovery" CR)); - int xxWSD0xMMCparametersCount = 5; - if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_DECR") != 0) xxWSD0xMMCparametersCount = 5; - if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) xxWSD0xMMCparametersCount = 7; - const char* xxWSD0xMMCsensor[xxWSD0xMMCparametersCount][9] = { - {"sensor", "Battery", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "Temperature", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "Humidity", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement}, - {"sensor", "RSSI", mac, "signal_strength", jsonRSSI, "", "", "dB", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement, state class - }; - if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_DECR") != 0) { // Encrypted PVVX don't have Voltage - const char* voltage[9] = {"sensor", "Voltage", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}; - memcpy(&xxWSD0xMMCsensor[4], voltage, sizeof(voltage)); - }; - if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) { - const char* power[9] = {"sensor", "Power", mac, "", jsonPower, "", "", "", stateClassNone}; - memcpy(&xxWSD0xMMCsensor[5], power, sizeof(power)); - const char* open[9] = {"sensor", "Opening", mac, "", jsonOpen, "", "", "", stateClassNone}; - memcpy(&xxWSD0xMMCsensor[6], open, sizeof(open)); - }; - createDiscoveryFromList(mac, xxWSD0xMMCsensor, xxWSD0xMMCparametersCount, name, "Xiaomi", sensorModel); -} - -# else -void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) {} -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 XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel_id) {} -void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) {} -# endif - -/* - Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleScan.cpp - Ported to Arduino ESP32 by Evandro Copercini - */ -// core task implementation thanks to https://techtutorialsx.com/2017/05/09/esp32-running-code-on-a-specific-core/ - -//core on which the BLE detection task will run -static int taskCore = 0; - -class ScanCallbacks : public NimBLEScanCallbacks { - void onResult(const NimBLEAdvertisedDevice* advertisedDevice) { - NimBLEAdvertisedDevice* ad = new NimBLEAdvertisedDevice(*advertisedDevice); - if (xQueueSend(BLEQueue, &ad, 0) != pdTRUE) { - Log.error(F("BLEQueue full" CR)); - delete (ad); - } - } -} scanCallbacks; - -std::string convertServiceData(std::string deviceServiceData) { - int serviceDataLength = (int)deviceServiceData.length(); - char spr[2 * serviceDataLength + 1]; - for (int i = 0; i < serviceDataLength; i++) sprintf(spr + 2 * i, "%.2x", (unsigned char)deviceServiceData[i]); - spr[2 * serviceDataLength] = 0; - Log.trace(F("Converted service data (%d) to %s" CR), serviceDataLength, spr); - return spr; -} - -bool checkIfIsTracker(char ch) { - uint8_t data = 0; - if (ch >= '0' && ch <= '9') - data = ch - '0'; - else if (ch >= 'a' && ch <= 'f') - data = 10 + (ch - 'a'); - - if (((data >> 3) & 0x01) == 1) { - Log.trace(F("Is Device Tracker" CR)); - return true; - } else { - return false; - } -} - -void procBLETask(void* pvParameters) { - BLEAdvertisedDevice* advertisedDevice = nullptr; - - for (;;) { - xQueueReceive(BLEQueue, &advertisedDevice, portMAX_DELAY); - // Feed the watchdog - //esp_task_wdt_reset(); - if (!BTProcessLock) { - Log.trace(F("Creating BLE buffer" CR)); - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = advertisedDevice->getAddress().toString(); - BLEdata["mac_type"] = advertisedDevice->getAddress().getType(); - BLEdata["adv_type"] = advertisedDevice->getAdvType(); - Log.notice(F("BT Device detected: %s" CR), BLEdata["id"].as()); - BLEdevice* device = getDeviceByMac(BLEdata["id"].as()); - - if (BTConfig.filterConnectable && device->connect) { - Log.notice(F("Filtered connectable device" CR)); - delete (advertisedDevice); - continue; - } - - if (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(device)) && !isBlack(device))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC) - if (advertisedDevice->haveName()) - BLEdata["name"] = (char*)advertisedDevice->getName().c_str(); - if (advertisedDevice->haveManufacturerData()) { - BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString((uint8_t*)advertisedDevice->getManufacturerData().data(), - advertisedDevice->getManufacturerData().length()); - } - BLEdata["rssi"] = (int)advertisedDevice->getRSSI(); - if (advertisedDevice->haveTXPower()) - BLEdata["txpower"] = (int8_t)advertisedDevice->getTXPower(); - if (BTConfig.presenceEnable) { - hass_presence(BLEdata); // with either only sensors or not we can use it for home assistant room presence component - } - if (advertisedDevice->haveServiceData()) { - int serviceDataCount = advertisedDevice->getServiceDataCount(); - Log.trace(F("Get services data number: %d" CR), serviceDataCount); - for (int j = 0; j < serviceDataCount; j++) { - StaticJsonDocument BLEdataBufferTemp; - JsonObject BLEdataTemp = BLEdataBufferTemp.to(); - BLEdataBufferTemp = BLEdataBuffer; - std::string service_data = convertServiceData(advertisedDevice->getServiceData(j)); - Log.trace(F("Service data: %s" CR), service_data.c_str()); - std::string serviceDatauuid = advertisedDevice->getServiceDataUUID(j).toString(); - Log.trace(F("Service data UUID: %s" CR), (char*)serviceDatauuid.c_str()); - BLEdataTemp["servicedata"] = (char*)service_data.c_str(); - BLEdataTemp["servicedatauuid"] = (char*)serviceDatauuid.c_str(); - PublishDeviceData(BLEdataTemp); - } - } else { - PublishDeviceData(BLEdata); - } - } else { - Log.trace(F("Filtered MAC device" CR)); - } - updateDevicesStatus(); - } - delete (advertisedDevice); - vTaskDelay(10); - } -} - -/** - * BLEscan used to retrieve BLE advertized data from devices without connection - */ -void BLEscan() { - // Don't start the next scan until processing of previous results is complete. - while (uxQueueMessagesWaiting(BLEQueue) || queueLength != 0) { // the criteria on queueLength could be adjusted to parallelize the scan and the queue processing - delay(1); // Wait for queue to empty, a yield here instead of the delay cause the WDT to trigger - } - Log.notice(F("Scan begin" CR)); - BLEScan* pBLEScan = BLEDevice::getScan(); - pBLEScan->setScanCallbacks(&scanCallbacks); - if ((millis() > (timeBetweenActive + BTConfig.intervalActiveScan) || BTConfig.intervalActiveScan == BTConfig.BLEinterval) && !BTConfig.forcePassiveScan) { - pBLEScan->setActiveScan(true); - timeBetweenActive = millis(); - } else { - pBLEScan->setActiveScan(false); - } - pBLEScan->setInterval(BLEScanInterval); - pBLEScan->setWindow(BLEScanWindow); - NimBLEScanResults foundDevices = pBLEScan->getResults(BTConfig.scanDuration, false); - if (foundDevices.getCount()) - scanCount++; - Log.notice(F("Found %d devices, scan number %d end" CR), foundDevices.getCount(), scanCount); - Log.trace(F("Process BLE stack free: %u" CR), uxTaskGetStackHighWaterMark(xProcBLETaskHandle)); -} - -/** - * Connect to BLE devices and initiate the callbacks with a service/characteristic request - */ -# if BLEDecoder -void BLEconnect() { - if (!BTProcessLock) { - Log.notice(F("BLE Connect begin" CR)); - do { - for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { - BLEdevice* p = *it; - if (p->connect) { - Log.trace(F("Model to connect found: %s" CR), p->macAdr); - NimBLEAddress addr((const char*)p->macAdr, p->macType); - if (p->sensorModel_id == BLEconectable::id::LYWSD03MMC || - p->sensorModel_id == BLEconectable::id::MHO_C401) { - LYWSD03MMC_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - BLEclient.publishData(); - } else if (p->sensorModel_id == BLEconectable::id::DT24_BLE) { - DT24_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - BLEclient.publishData(); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { - BM2_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); - BLEclient.publishData(); - } else if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { - XMWSDJ04MMC_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - BLEclient.publishData(); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1) { - SBS1_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT) { - SBBT_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU) { - SBCU_connect BLEclient(addr); - BLEclient.processActions(BLEactions); - } else { - GENERIC_connect BLEclient(addr); - if (BLEclient.processActions(BLEactions)) { - // If we don't regularly connect to this, disable connections so advertisements - // won't be filtered if BLE_FILTER_CONNECTABLE is set. - p->connect = false; - } - } - if (BLEactions.size() > 0) { - std::vector swap; - for (auto& it : BLEactions) { - if (!it.complete && --it.ttl) { - swap.push_back(it); - } else if (it.addr == NimBLEAddress(p->macAdr, p->macType)) { - if (p->sensorModel_id != BLEconectable::id::DT24_BLE && - 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 != BLEconectable::id::MHO_C401 && - p->sensorModel_id != BLEconectable::id::XMWSDJ04MMC) { - // if irregulary connected to and connection failed clear the connect flag. - p->connect = false; - } - } - } - std::swap(BLEactions, swap); - } - } - } - } while (BLEactions.size() > 0); - Log.notice(F("BLE Connect end" CR)); - } -} -# else -void BLEconnect() {} -# endif - -void stopProcessing(bool deinit) { - if (BTConfig.enabled) { - BTProcessLock = true; - // We stop the scan - Log.notice(F("Stopping BLE scan" CR)); - BLEScan* pBLEScan = BLEDevice::getScan(); - if (pBLEScan->isScanning()) { - pBLEScan->stop(); - } - - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { - Log.notice(F("Stopping BLE tasks" CR)); - //Suspending, deleting tasks and stopping BT to free memory - vTaskSuspend(xCoreTaskHandle); - vTaskDelete(xCoreTaskHandle); - vTaskSuspend(xProcBLETaskHandle); - vTaskDelete(xProcBLETaskHandle); - xSemaphoreGive(semaphoreBLEOperation); - } - // Using deinit to free memory, should only be used if we are going to restart the gateway - if (deinit) - BLEDevice::deinit(true); - } - Log.notice(F("BLE gateway stopped, free heap: %d" CR), ESP.getFreeHeap()); -} - -void coreTask(void* pvParameters) { - while (true) { - if (!BTProcessLock) { - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(30000)) == pdTRUE) { - BLEscan(); - // Launching a connect every TimeBtwConnect - if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { - timeBetweenConnect = millis(); - BLEconnect(); - } - //dumpDevices(); - Log.trace(F("CoreTask stack free: %u" CR), uxTaskGetStackHighWaterMark(xCoreTaskHandle)); - xSemaphoreGive(semaphoreBLEOperation); - } else { - Log.error(F("Failed to start scan - BLE busy" CR)); - } - if (SYSConfig.powerMode > 0) { - int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); // is this enough, it will wait the full deepsleep... - if (scan == 1) BTforceScan(); - ready_to_sleep = true; - } else { - for (int interval = BTConfig.BLEinterval, waitms; interval > 0; interval -= waitms) { - int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); - if (scan == 1) BTforceScan(); // should we break after this? - delay(waitms = interval > 100 ? 100 : interval); // 100ms - } - } - } - delay(1); - } -} - -void setupBTTasksAndBLE() { -# ifdef CONFIG_BTDM_BLE_SCAN_DUPL - BLEDevice::setScanDuplicateCacheSize(BLEScanDuplicateCacheSize); -# endif - BLEDevice::init(""); - xTaskCreateUniversal( - procBLETask, /* Function to implement the task */ - "procBLETask", /* Name of the task */ -# if defined(USE_ESP_IDF) || defined(USE_BLUFI) - 14500, -# else - 9500, /* Stack size in bytes */ -# endif - NULL, /* Task input parameter */ - 2, /* Priority of the task (set higher than core task) */ - &xProcBLETaskHandle, /* Task handle. */ - 1); /* Core where the task should run */ - - // we setup a task with priority one to avoid conflict with other gateways - xTaskCreateUniversal( - coreTask, /* Function to implement the task */ - "coreTask", /* Name of the task */ - 5120, /* Stack size in bytes */ - NULL, /* Task input parameter */ - 1, /* Priority of the task */ - &xCoreTaskHandle, /* Task handle. */ - taskCore); /* Core where the task should run */ -} - -void setupBT() { - BTConfig_init(); - BTConfig_load(); - Log.notice(F("BLE scans interval: %d" CR), BTConfig.BLEinterval); - Log.notice(F("BLE connects interval: %d" CR), BTConfig.intervalConnect); - Log.notice(F("BLE scan duration: %d" CR), BTConfig.scanDuration); - Log.notice(F("Publishing only BLE sensors: %T" CR), BTConfig.pubOnlySensors); - Log.notice(F("Publishing random MAC devices: %T" CR), BTConfig.pubRandomMACs); - Log.notice(F("Adaptive BLE scan: %T" CR), BTConfig.adaptiveScan); - Log.notice(F("Active BLE scan interval: %d" CR), BTConfig.intervalActiveScan); - Log.notice(F("minrssi: %d" CR), -abs(BTConfig.minRssi)); - Log.notice(F("Presence Away Timer: %d" CR), BTConfig.presenceAwayTimer); - Log.notice(F("Moving Timer: %d" CR), BTConfig.movingTimer); - Log.notice(F("Force passive scan: %T" CR), BTConfig.forcePassiveScan); - Log.notice(F("Enabled BLE: %T" CR), BTConfig.enabled); - - atomic_init(&forceBTScan, 0); // in theory, we don't need this - - semaphoreCreateOrUpdateDevice = xSemaphoreCreateBinary(); - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - - semaphoreBLEOperation = xSemaphoreCreateBinary(); - xSemaphoreGive(semaphoreBLEOperation); - - BLEQueue = xQueueCreate(QueueSize, sizeof(NimBLEAdvertisedDevice*)); - if (BTConfig.enabled) { - setupBTTasksAndBLE(); - Log.notice(F("gatewayBT multicore ESP32 setup done" CR)); - } else { - Log.notice(F("gatewayBT multicore ESP32 setup disabled" CR)); - } -} - -boolean valid_service_data(const char* data, int size) { - for (int i = 0; i < size; ++i) { - if (data[i] != 48) // 48 correspond to 0 in ASCII table - return true; - } - return false; -} - -# if defined(ZmqttDiscovery) && BLEDecoder == true -// This function always should be called from the main core as it generates direct mqtt messages -// When overrideDiscovery=true, we publish discovery messages of known devices (even if no new) -void launchBTDiscovery(bool overrideDiscovery) { - if (!overrideDiscovery && newDevices == 0) - return; - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdFALSE) { - Log.error(F("Semaphore NOT taken" CR)); - return; - } - newDevices = 0; - vector localDevices = devices; - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - for (vector::iterator it = localDevices.begin(); it != localDevices.end(); ++it) { - BLEdevice* p = *it; - Log.trace(F("Device mac %s" CR), p->macAdr); - // Do not launch discovery for the devices already discovered (unless we have overrideDiscovery) or that are not unique by their MAC Address (iBeacon, GAEN and Microsoft CDP) - if (overrideDiscovery || !isDiscovered(p)) { - String macWOdots = String(p->macAdr); - macWOdots.replace(":", ""); - if (p->sensorModel_id >= 0) { - Log.trace(F("Looking for Model_id: %d" CR), p->sensorModel_id); - std::string properties = decoder.getTheengProperties(p->sensorModel_id); - Log.trace(F("properties: %s" CR), properties.c_str()); - std::string brand = decoder.getTheengAttribute(p->sensorModel_id, "brand"); - std::string model = decoder.getTheengAttribute(p->sensorModel_id, "model"); - if (displayDeviceName || ForceDeviceName) { - if (p->name[0] != '\0') { - model = p->name; - } - } - std::string model_id = decoder.getTheengAttribute(p->sensorModel_id, "model_id"); - - // Check for tracker status - bool isTracker = false; - std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); - if (tag.length() >= 4) { - isTracker = checkIfIsTracker(tag[3]); - } - - String discovery_topic = String(subjectBTtoMQTT) + "/" + macWOdots; - 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 - if (isTracker) { - String tracker_name = String(model_id.c_str()) + "-tracker"; - String tracker_id = macWOdots + "-tracker"; - createDiscovery("device_tracker", - discovery_topic.c_str(), tracker_name.c_str(), 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 == TheengsDecoder::BLE_ID_NUM::BC08) { - String sensor_name = String(model_id.c_str()) + "-moving"; - String sensor_id = macWOdots + "-moving"; - createDiscovery("binary_sensor", - discovery_topic.c_str(), sensor_name.c_str(), sensor_id.c_str(), - will_Topic, "moving", "{% if value_json.get('accx') -%}on{%- else -%}off{%- endif %}", - "on", "off", "", - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } - if (displayDeviceName && p->sensorModel_id >= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_ATC && p->sensorModel_id <= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_PVVX_BTHOME_2 ) { - xxWSD0xMMCDiscovery(macWOdots.c_str(), p->name, model_id.c_str()); - } else if (!properties.empty()) { - StaticJsonDocument jsonBuffer; - auto error = deserializeJson(jsonBuffer, properties); - if (error) { - if (jsonBuffer.overflowed()) { - // This should not happen if JSON_MSG_BUFFER is large enough for - // the Theengs json properties - Log.error(F("JSON deserialization of Theengs properties overflowed (error %s), buffer capacity: %u. Program might crash. Properties json: %s" CR), - error.c_str(), jsonBuffer.capacity(), properties.c_str()); - } else { - Log.error(F("JSON deserialization of Theengs properties errored: %" CR), - error.c_str()); - } - } - for (JsonPair prop : jsonBuffer["properties"].as()) { - Log.trace(F("Key: %s"), prop.key().c_str()); - Log.trace(F("Unit: %s"), prop.value()["unit"].as()); - Log.trace(F("Name: %s"), prop.value()["name"].as()); - String entity_name = ""; - if (displayDeviceName || ForceDeviceName) { - entity_name = String(model.c_str()) + "-" + String(prop.key().c_str()); - } else { - entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); - } - String unique_id = macWOdots + "-" + String(prop.key().c_str()); - String value_template = "{{ value_json." + String(prop.key().c_str()) + " | is_defined }}"; - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1 && strcmp(prop.key().c_str(), "state") == 0) { - String payload_on = "{\"model_id\":\"X1\",\"cmd\":\"on\",\"id\":\"" + String(p->macAdr) + "\"}"; - String payload_off = "{\"model_id\":\"X1\",\"cmd\":\"off\",\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("switch", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "switch", value_template.c_str(), - payload_on.c_str(), payload_off.c_str(), "", 0, - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone, "off", "on"); - unique_id = macWOdots + "-press"; - entity_name = String(model_id.c_str()) + "-press"; - String payload_press = "{\"model_id\":\"X1\",\"cmd\":\"press\",\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("button", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "button", "", - payload_press.c_str(), "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT && strcmp(prop.key().c_str(), "open") == 0) { - value_template = "{% if value_json.direction == \"up\" -%} {{ 100 - value_json.open/2 }}{% elif value_json.direction == \"down\" %}{{ value_json.open/2 }}{% else %} {{ value_json.open/2 }}{%- endif %}"; - String command_template = "{\"model_id\":\"W270160X\",\"tilt\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("cover", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "cover", value_template.c_str(), - "50", "", "", 0, - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - "blind", nullptr, nullptr, nullptr, command_template.c_str()); - } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU && strcmp(prop.key().c_str(), "position") == 0) { - String command_template = "{\"model_id\":\"W070160X\",\"position\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; - createDiscovery("cover", //set Type - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "cover", "{{ value_json.position }}", - "0", "100", "", 0, - Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - "curtain", nullptr, nullptr, nullptr, command_template.c_str()); - } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && - strcmp(prop.key().c_str(), "weighing_mode") == 0) { - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "enum", value_template.c_str(), - "", "", prop.value()["unit"], - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassMeasurement, nullptr, nullptr, "[\"person\",\"object\"]"); - } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && - strcmp(prop.key().c_str(), "unit") == 0) { - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, "enum", value_template.c_str(), - "", "", prop.value()["unit"], - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassMeasurement, nullptr, nullptr, "[\"lb\",\"kg\",\"jin\"]"); - } else if (strcmp(prop.value()["unit"], "string") == 0 && strcmp(prop.key().c_str(), "mac") != 0) { - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "", "", "", - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (p->sensorModel_id == TheengsDecoder::MUE4094RT && strcmp(prop.value()["unit"], "status") == 0) { // This device does not a broadcast when there is nothing detected so adding a timeout - createDiscovery("binary_sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "True", "False", "", - BTConfig.presenceAwayTimer / 1000, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (strcmp(prop.value()["unit"], "status") == 0) { - createDiscovery("binary_sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "True", "False", "", - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassNone); - } else if (strcmp(prop.key().c_str(), "device") != 0 && strcmp(prop.key().c_str(), "mac") != 0) { // Exception on device and mac as these ones are not sensors - createDiscovery("sensor", - discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), - will_Topic, prop.value()["name"], value_template.c_str(), - "", "", prop.value()["unit"], - 0, "", "", false, "", - model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, - stateClassMeasurement); - } - } - } - } 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) { - // 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"); - } - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { - // Sensor discovery - BM2Discovery(macWOdots.c_str(), "BM2"); - // Device tracker discovery - String tracker_id = macWOdots + "-tracker"; - createDiscovery("device_tracker", - discovery_topic.c_str(), "BM2-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"); - } - if (p->sensorModel_id == BLEconectable::id::MHO_C401) { - MHO_C401Discovery(macWOdots.c_str(), "MHO-C401"); - } - if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { - XMWSDJ04MMCDiscovery(macWOdots.c_str(), "XMWSDJ04MMC"); - } - if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC) { - HHCCJCY01HHCCDiscovery(macWOdots.c_str(), "HHCCJCY01HHCC"); - } - } else { - Log.trace(F("Device UNKNOWN_MODEL %s" CR), p->macAdr); - } - } - } - p->isDisc = true; // we don't need the semaphore and all the search magic via createOrUpdateDevice - } else { - Log.trace(F("Device already discovered or that doesn't require discovery %s" CR), p->macAdr); - } - } -} -# else -void launchBTDiscovery(bool overrideDiscovery) {} -# endif - -# if BLEDecryptor -// ** TODO - Hex string to bytes, there is probably a function for this already just need to find it -int hexToBytes(String hex, uint8_t *out, size_t maxLen) { - int len = hex.length(); - if (len % 2 || len / 2 > maxLen) return -1; - for (int i = 0, j = 0; i < len; i += 2, j++) { - out[j] = (uint8_t) strtol(hex.substring(i, i + 2).c_str(), nullptr, 16); - } - return len / 2; -} -// Reverse bytes -void reverseBytes(uint8_t *data, size_t length) { - size_t i; - for (i = 0; i < length / 2; i++) { - uint8_t temp = data[i]; - data[i] = data[length - 1 - i]; - data[length - 1 - i] = temp; - } -} -# endif - -# if BLEDecoder -void process_bledata(JsonObject& BLEdata) { - yield(); // Necessary to let the loop run in case of connectivity issues - if (!BLEdata.containsKey("id")) { - Log.error(F("No mac address in the payload" CR)); - return; - } - const char* mac = BLEdata["id"].as(); - Log.trace(F("Processing BLE data %s" CR), BLEdata["id"].as()); - int model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); - int mac_type = BLEdata["mac_type"].as(); - -# if BLEDecryptor - if (BLEdata["encr"] && (BLEdata["encr"].as() >0 && BLEdata["encr"].as() <=2)) { - // Decrypting Encrypted BLE Data PVVX, BTHome or Victron - Log.trace(F("[BLEDecryptor] Decrypt ENCR:%d ModelID:%s Payload:%s" CR), BLEdata["encr"].as(), BLEdata["model_id"].as(), BLEdata["cipher"].as()); - - // MAC address - String macWOdots = BLEdata["id"].as(); // Mac Address without dots - macWOdots.replace(":", ""); - unsigned char macAddress[6]; - int maclen = hexToBytes(macWOdots, macAddress, 6); - if (maclen != 6) { - Log.error(F("[BLEDecryptor] Invalid MAC Address length %d" CR), maclen); - return; - } - - // AES decryption key - unsigned char bleaeskey[16]; - int bleaeskeylength = 0; - if (ble_aes_keys.containsKey(macWOdots)){ - Log.trace(F("[BLEDecryptor] Custom AES key %s" CR), ble_aes_keys[macWOdots].as()); - bleaeskeylength = hexToBytes(ble_aes_keys[macWOdots], bleaeskey, 16); - } else { - Log.trace(F("[BLEDecryptor] Default AES key" CR)); - bleaeskeylength = hexToBytes(ble_aes, bleaeskey, 16); - } - // Check AES Key - if (bleaeskeylength != 16) { - Log.error(F("[BLEDecryptor] Invalid key length %d" CR), bleaeskeylength); - return; - } - - // Build nonce and aad - uint8_t nonce[16]; - int noncelength = 0; - unsigned char aad[1]; - int aadLength; - - if (BLEdata["encr"].as() == 1){ // PVVX Encrypted - noncelength = 11; // 11 bytes - reverseBytes(macAddress, 6); // 6 bytes: device address in reverse - memcpy(nonce, macAddress, 6); - int maclen = hexToBytes(macWOdots, macAddress, 6); - - unsigned char servicedata[16]; - int servicedatalen = hexToBytes(BLEdata["servicedata"].as(), servicedata, 16); - nonce[6] = servicedatalen + 3; // 1 byte : length of (service data + type and UUID) - nonce[7] = 0x16; // 1 byte : "16" -> AD type for "Service Data - 16-bit UUID" - nonce[8] = 0x1A; // 2 bytes: "1a18" -> UUID 181a in little-endian - nonce[9] = 0x18; // - unsigned char ctr[1]; // 1 byte : counter - int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 1); - if (ctrlen != 1) { - Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); - return; - } - nonce[10] = ctr[0]; - aad[0] = 0x11; - aadLength = 1; - Log.trace(F("[BLEDecryptor] PVVX nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); - - } else if (BLEdata["encr"].as() == 2){ // BTHome V2 Encrypted - noncelength = 13; // 13 bytes - memcpy(nonce, macAddress, 6); - nonce[6] = 0xD2; // UUID - nonce[7] = 0xFC; - nonce[8] = 0x41; // BTHome Device Data encrypted payload byte - unsigned char ctr[4]; // Counter - int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 4); - if (ctrlen != 4) { - Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); - return; - } - memcpy(&nonce[9], ctr, 4); - aad[0] = 0x00; - aadLength = 0; - Log.trace(F("[BLEDecryptor] BTHomeV2 nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); - - } else if (BLEdata["encr"].as() == 3){ - nonce[16] = {0}; // Victron has a 16 byte zero padded nonce with IV bytes 6,7 - unsigned char iv[2]; - int ivlen = hexToBytes(BLEdata["ctr"].as(), iv, 2); - if (ivlen != 2) { - Log.error(F("[BLEDecryptor] Invalid iv length %d" CR), ivlen); - return; - } - memcpy(nonce, iv, 2); - Log.trace(F("[BLEDecryptor] Victron nonce %s" CR), NimBLEUtils::dataToHexString(nonce, 16).c_str()); - } else { - return; // No match - } - - // Ciphertext to bytes - int cipherlen = sizeof(BLEdata["cipher"].as()); - unsigned char ciphertext[cipherlen]; - int ciphertextlen = hexToBytes(BLEdata["cipher"].as(), ciphertext, cipherlen); - unsigned char decrypted[ciphertextlen]; // Decrypted payload - - // Decrypt ciphertext - if (BLEdata["encr"].as() == 1 || BLEdata["encr"].as() == 2) { - // Decrypt PVVX and BTHome V2 ciphertext using AES CCM - mbedtls_ccm_context ctx; - mbedtls_ccm_init(&ctx); - if (mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, bleaeskey, 128) != 0) { - Log.error(F("[BLEDecryptor] Failed to set AES key to mbedtls" CR)); - return; - } - - // Message Integrity Check (MIC) - unsigned char mic[4]; - int miclen = hexToBytes(BLEdata["mic"].as(), mic, 4); - if (miclen != 4) { - Log.error(F("[BLEDecryptor] Invalid MIC length %d" CR), miclen); - return; - } - - int ret = mbedtls_ccm_auth_decrypt( - &ctx, // AES Key - ciphertextlen, // length of ciphertext - nonce, noncelength, // Nonce - aad, aadLength, // AAD - ciphertext, // input ciphertext - decrypted, // output plaintext - mic, sizeof(mic) // Message Integrity Check - ); - mbedtls_ccm_free(&ctx); - - if (ret == 0) { - Log.notice(F("[BLEDecryptor] Decryption successful" CR)); - } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { - Log.error(F("[BLEDecryptor] Authentication failed." CR)); - return; - } else { - Log.error(F("[BLEDecryptor] Decryption failed with error: %X" CR), ret); - return; - } - - // Build new servicedata - if (BLEdata["encr"].as() == 1){ // PVVX - BLEdata["servicedata"] = NimBLEUtils::dataToHexString(decrypted, ciphertextlen); - } else if (BLEdata["encr"].as() == 2) { // BTHomeV2 - // Build new servicedata - uint8_t newservicedata[3 + ciphertextlen]; - newservicedata[0] = 0x40; // Decrypted BTHomeV2 Packet Type - newservicedata[1] = 0x00; // Packet counter which the PVVX BTHome non-encrypted has but the encrypted does not - newservicedata[2] = 0x00; // **TODO Convert the ctr to the packet counter or just stick with 0? - memcpy(&newservicedata[3], decrypted, ciphertextlen); - BLEdata["servicedata"] = NimBLEUtils::dataToHexString(newservicedata, ciphertextlen + 3); - } else { - return; - } - Log.trace(F("[BLEDecryptor] Decrypted servicedata %s" CR), BLEdata["servicedata"].as()); - - } else if (BLEdata["encr"].as() == 3) { - // Decrypt Victron Energy encrypted advertisements. - size_t nc_off = 0; - uint8_t stream_block[16] = {0}; - - mbedtls_aes_context ctx; - mbedtls_aes_init(&ctx); - mbedtls_aes_setkey_enc(&ctx, bleaeskey, 128); - int ret = mbedtls_aes_crypt_ctr( - &ctx, // AES Key - ciphertextlen, // length of ciphertext - &nc_off, - nonce, // 16 byte nonce with 2 bytes iv - stream_block, - ciphertext, // input ciphertext - decrypted // output plaintext - ); - mbedtls_aes_free(&ctx); - - if (ret == 0) { - Log.notice(F("[BLEDecryptor] Victron Decryption successful" CR)); - } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { - Log.error(F("[BLEDecryptor] Victron Authentication failed." CR)); - return; - } else { - Log.error(F("[BLEDecryptor] Victron decryption failed with error: %X" CR), ret); - return; - } - - // Build new manufacturerdata - unsigned char manufacturerdata[10 + ciphertextlen]; - int manufacturerdatalen = hexToBytes(BLEdata["manufacturerdata"].as(), manufacturerdata, 10); - manufacturerdata[2] = 0x11; // Replace byte 2 with "11" indicate decrypted data - manufacturerdata[7] = 0xff; // Replace byte 7 with "ff" to indicate decrypted data - manufacturerdata[8] = 0xff; // Replace byte 8 with "ff" to indicate decrypted data - memcpy(&manufacturerdata[8], decrypted, ciphertextlen); // Append the decrypted payload to the manufacturer data - BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString(manufacturerdata, 10 + ciphertextlen); // Rebuild manufacturerdata - Log.trace(F("[BLEDecryptor] Victron decrypted manufacturerdata %s" CR), BLEdata["manufacturerdata"].as()); - } - - // Print before and after decoder post decryption - // serializeJsonPretty(BLEdata, Serial); - model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); - // serializeJsonPretty(BLEdata, Serial); - Log.trace(F("[BLEDecryptor] Decrypted model_id %d" CR), model_id); - - // Remove the cipher fields from BLEdata - BLEdata.remove("encr"); - BLEdata.remove("cipher"); - BLEdata.remove("ctr"); - BLEdata.remove("mic"); - - } -# endif - - // Convert prmacs to RMACS until or if OMG gets Identity MAC/IRK decoding - if (BLEdata["prmac"]) { - BLEdata.remove("prmac"); - if (BLEdata["track"]) { - BLEdata.remove("track"); - } - BLEdata["type"] = "RMAC"; - Log.trace(F("Potential RMAC (prmac) converted to RMAC" CR)); - } - const char* deviceName = BLEdata["name"] | ""; - - 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 - 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 - createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); - } else { - createOrUpdateDevice(mac, device_flags_init, model_id, mac_type, deviceName); - if (BTConfig.adaptiveScan == true && (BTConfig.BLEinterval != MinTimeBtwScan || BTConfig.intervalActiveScan != MinTimeBtwScan)) { - if (BLEdata.containsKey("acts") && BLEdata.containsKey("cont")) { - if (BLEdata["acts"] && BLEdata["cont"]) { - BTConfig.BLEinterval = MinTimeBtwScan; - BTConfig.intervalActiveScan = MinTimeBtwScan; - BTConfig.scanDuration = MinScanDuration; - Log.notice(F("Active and continuous scanning required, parameters adapted" CR)); - // stateBTMeasures(false); - } - } else if (BLEdata.containsKey("cont") && BTConfig.BLEinterval != MinTimeBtwScan) { - if (BLEdata["cont"]) { - BTConfig.BLEinterval = MinTimeBtwScan; - if ((BLEdata["type"].as()).compare("CTMO") == 0) { - BTConfig.scanDuration = MinScanDuration; - } - Log.notice(F("Passive continuous scanning required, parameters adapted" CR)); - // stateBTMeasures(false); - } - } - } - } - } else { - if (BLEdata.containsKey("name")) { // Connectable only devices - std::string name = BLEdata["name"]; - if (name.compare("LYWSD03MMC") == 0) - model_id = BLEconectable::id::LYWSD03MMC; - else if (name.compare("DT24-BLE") == 0) - model_id = BLEconectable::id::DT24_BLE; - else if (name.compare("MHO-C401") == 0) - model_id = BLEconectable::id::MHO_C401; - else if (name.compare("XMWSDJ04MMC") == 0) - model_id = BLEconectable::id::XMWSDJ04MMC; - - if (model_id > 0) { - Log.trace(F("Connectable device found: %s" CR), name.c_str()); - createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); - } - } else if (BTConfig.extDecoderEnable && model_id < 0 && BLEdata.containsKey("servicedata")) { - const char* service_data = (const char*)(BLEdata["servicedata"] | ""); - if (strstr(service_data, "209800") != NULL) { - model_id = TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC; - Log.trace(F("Connectable device found: HHCCJCY01HHCC" CR)); - createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); - } - } - } - } else { - Log.trace(F("Random MAC or iBeacon device filtered" CR)); - } - if (!BTConfig.extDecoderEnable && model_id < 0) { - Log.trace(F("No eligible device found " CR)); - } -} -void PublishDeviceData(JsonObject& BLEdata) { - if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough - // Decode the payload - process_bledata(BLEdata); - // If the device is a random MAC and pubRandomMACs is false we don't publish this payload - if (!BTConfig.pubRandomMACs && (BLEdata["type"].as()).compare("RMAC") == 0) { - Log.trace(F("Random MAC, device filtered" CR)); - return; - } - // If pubAdvData is false we don't publish the adv data - if (!BTConfig.pubAdvData) { - BLEdata.remove("servicedatauuid"); - BLEdata.remove("servicedata"); - BLEdata.remove("manufacturerdata"); - BLEdata.remove("mac_type"); - BLEdata.remove("adv_type"); - // tag device properties - // BLEdata.remove("type"); type is used by the WebUI module to determine the template used to display the signal - BLEdata.remove("cidc"); - BLEdata.remove("acts"); - BLEdata.remove("cont"); - BLEdata.remove("track"); - BLEdata.remove("ctrl"); - } - // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid - if (BLEdata.containsKey("distance")) { - if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { - BLEdata["mac"] = BLEdata["id"].as(); - BLEdata["id"] = BLEdata["uuid"].as(); - } - String topic = String(mqtt_topic) + BTConfig.presenceTopic + String(gateway_name); - Log.trace(F("Pub HA Presence %s" CR), topic.c_str()); - BLEdata["topic"] = topic; - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } - - // If the device is not a sensor and pubOnlySensors is true we don't publish this payload - if (!BTConfig.pubOnlySensors || BLEdata.containsKey("model") || !BLEDecoder) { // Identified device - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } else { - Log.notice(F("Not a sensor device filtered" CR)); - return; - } - -# if BLEDecoder - if (enableMultiGTWSync && BLEdata.containsKey("model_id") && BLEdata.containsKey("id")) { - // Publish tracker sync message - bool isTracker = false; - std::string tag = decoder.getTheengAttribute(BLEdata["model_id"].as(), "tag"); - if (tag.length() >= 4) { - isTracker = checkIfIsTracker(tag[3]); - } - - if (isTracker) { - StaticJsonDocument BLEdataBuffer; - JsonObject TrackerSyncdata = BLEdataBuffer.to(); - TrackerSyncdata["gatewayid"] = gateway_name; - TrackerSyncdata["trackerid"] = BLEdata["id"].as(); - String topic = String(mqtt_topic) + String(subjectTrackerSync); - TrackerSyncdata["topic"] = topic.c_str(); - enqueueJsonObject(TrackerSyncdata); - } - } -# endif - } else { - Log.notice(F("Low rssi, device filtered" CR)); - return; - } -} -# else -void process_bledata(JsonObject& BLEdata) {} -void PublishDeviceData(JsonObject& BLEdata) { - if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough - // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid - if (BLEdata.containsKey("distance")) { - if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { - BLEdata["mac"] = BLEdata["id"].as(); - BLEdata["id"] = BLEdata["uuid"].as(); - } - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - } else { - Log.notice(F("Low rssi, device filtered" CR)); - return; - } -} -# endif - -void hass_presence(JsonObject& HomePresence) { - int BLErssi = HomePresence["rssi"]; - Log.trace(F("BLErssi %d" CR), BLErssi); - int txPower = HomePresence["txpower"] | 0; - if (txPower >= 0) - txPower = -59; //if tx power is not found we set a default calibration value - Log.trace(F("TxPower: %d" CR), txPower); - double ratio = BLErssi * 1.0 / txPower; - double distance; - if (ratio < 1.0) { - distance = pow(ratio, 10); - } else { - distance = (0.89976) * pow(ratio, 7.7095) + 0.111; - } - HomePresence["distance"] = distance; - Log.trace(F("Ble distance %D" CR), distance); -} - -void BTforceScan() { - if (!BTProcessLock) { - BLEscan(); - Log.trace(F("Scan done" CR)); - if (BTConfig.bleConnect) - BLEconnect(); - } else { - Log.trace(F("Cannot launch scan due to other process running" CR)); - } -} - -void immediateBTAction(void* pvParameters) { - if (BLEactions.size()) { - // Immediate action; we need to prevent the normal connection action and stop scanning - BTProcessLock = true; - NimBLEScan* pScan = NimBLEDevice::getScan(); - if (pScan->isScanning()) { - pScan->stop(); - } - - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { - // swap the vectors so only this device is processed - std::vector dev_swap; - dev_swap.push_back(getDeviceByMac(BLEactions.back().addr.toString().c_str())); - std::swap(devices, dev_swap); - - std::vector act_swap; - act_swap.push_back(BLEactions.back()); - BLEactions.pop_back(); - std::swap(BLEactions, act_swap); - - // Unlock here to allow the action to be performed - BTProcessLock = false; - BLEconnect(); - // back to normal - std::swap(devices, dev_swap); - std::swap(BLEactions, act_swap); - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - } else { - Log.error(F("CreateOrUpdate Semaphore NOT taken" CR)); - } - - // If we stopped the scheduled connect for this action, do the scheduled now - if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { - timeBetweenConnect = millis(); - BLEconnect(); - } - xSemaphoreGive(semaphoreBLEOperation); - } else { - Log.error(F("BLE busy - immediateBTAction not sent" CR)); - gatewayState = GatewayState::ERROR; - StaticJsonDocument BLEdataBuffer; - JsonObject BLEdata = BLEdataBuffer.to(); - BLEdata["id"] = BLEactions.back().addr.toString(); - BLEdata["success"] = false; - buildTopicFromId(BLEdata, subjectBTtoMQTT); - enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); - BLEactions.pop_back(); - BTProcessLock = false; - } - } - vTaskDelete(NULL); -} - -void startBTActionTask() { - TaskHandle_t th; - xTaskCreateUniversal( - immediateBTAction, /* Function to implement the task */ - "imActTask", /* Name of the task */ - 8000, /* Stack size in bytes */ - NULL, /* Task input parameter */ - 3, /* Priority of the task (set higher than core task) */ - &th, /* Task handle. */ - 1); /* Core where the task should run */ -} - -# if BLEDecoder -void KnownBTActions(JsonObject& BTdata) { - if (!BTdata.containsKey("id")) { - Log.error(F("BLE mac address missing" CR)); - gatewayState = GatewayState::ERROR; - return; - } - - BLEAction action{}; - action.write = true; - action.ttl = 3; - bool res = false; - if (BTdata.containsKey("model_id") && BTdata["model_id"].is()) { - if (BTdata["model_id"] == "X1") { - if (BTdata.containsKey("cmd") && BTdata["cmd"].is()) { - action.value_type = BLE_VAL_STRING; - std::string val = BTdata["cmd"].as(); // Fix #1694 - action.value = val; - createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, - TheengsDecoder::BLE_ID_NUM::SBS1, 1); - res = true; - } - } else if (BTdata["model_id"] == "W270160X") { - if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { - action.value_type = BLE_VAL_INT; - res = true; - } else if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { - action.value_type = BLE_VAL_STRING; - res = true; - } - if (res) { - std::string val = BTdata["tilt"].as(); // Fix #1694 - action.value = val; - createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, - TheengsDecoder::BLE_ID_NUM::SBBT, 1); - } - } else if (BTdata["model_id"] == "W070160X") { - if (BTdata.containsKey("position") && BTdata["position"].is()) { - action.value_type = BLE_VAL_INT; - res = true; - } else if (BTdata.containsKey("position") && BTdata["position"].is()) { - action.value_type = BLE_VAL_STRING; - res = true; - } - if (res) { - std::string val = BTdata["position"].as(); // Fix #1694 - action.value = val; - createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, - TheengsDecoder::BLE_ID_NUM::SBCU, 1); - } - } - if (res) { - action.addr = NimBLEAddress(BTdata["id"].as(), 1); - BLEactions.push_back(action); - startBTActionTask(); - } else { - Log.error(F("BLE action not recognized" CR)); - gatewayState = GatewayState::ERROR; - } - } -} -# else -void KnownBTActions(JsonObject& BTdata) {} -# endif - -void XtoBTAction(JsonObject& BTdata) { - BLEAction action{}; - action.ttl = BTdata.containsKey("ttl") ? (uint8_t)BTdata["ttl"] : 1; - action.value_type = BLE_VAL_STRING; - if (BTdata.containsKey("value_type")) { - String vt = BTdata["value_type"]; - vt.toUpperCase(); - if (vt == "HEX") - action.value_type = BLE_VAL_HEX; - else if (vt == "INT") - action.value_type = BLE_VAL_INT; - else if (vt == "FLOAT") - action.value_type = BLE_VAL_FLOAT; - else if (vt != "STRING") { - Log.error(F("BLE value type invalid %s" CR), vt.c_str()); - return; - } - } - - Log.trace(F("BLE ACTION TTL = %u" CR), action.ttl); - action.complete = false; - if (BTdata.containsKey("ble_write_address") && - BTdata.containsKey("ble_write_service") && - BTdata.containsKey("ble_write_char") && - BTdata.containsKey("ble_write_value")) { - action.addr = NimBLEAddress(BTdata["ble_write_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); - action.service = NimBLEUUID((const char*)BTdata["ble_write_service"]); - action.characteristic = NimBLEUUID((const char*)BTdata["ble_write_char"]); - std::string val = BTdata["ble_write_value"].as(); // Fix #1694 - action.value = val; - action.write = true; - Log.trace(F("BLE ACTION Write" CR)); - } else if (BTdata.containsKey("ble_read_address") && - BTdata.containsKey("ble_read_service") && - BTdata.containsKey("ble_read_char")) { - action.addr = NimBLEAddress(BTdata["ble_read_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); - action.service = NimBLEUUID((const char*)BTdata["ble_read_service"]); - action.characteristic = NimBLEUUID((const char*)BTdata["ble_read_char"]); - action.write = false; - Log.trace(F("BLE ACTION Read" CR)); - } else { - return; - } - - createOrUpdateDevice(action.addr.toString().c_str(), device_flags_connect, UNKWNON_MODEL, action.addr.getType()); - - BLEactions.push_back(action); - if (BTdata.containsKey("immediate") && BTdata["immediate"].as()) { - startBTActionTask(); - } -} - -void XtoBT(const char* topicOri, JsonObject& BTdata) { // json object decoding - if (cmpToMainTopic(topicOri, subjectMQTTtoBTset)) { - Log.trace(F("MQTTtoBT json set" CR)); - - // Black list & white list set - bool WorBupdated; - WorBupdated = updateWorB(BTdata, true); - WorBupdated |= updateWorB(BTdata, false); - - if (WorBupdated) { - if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { - //dumpDevices(); - xSemaphoreGive(semaphoreCreateOrUpdateDevice); - } - } - - // Force scan now - if (BTdata.containsKey("interval") && BTdata["interval"] == 0) { - Log.notice(F("BLE forced scan" CR)); - atomic_store_explicit(&forceBTScan, 1, ::memory_order_seq_cst); // ask the other core to do the scan for us - } - - /* - * Configuration modifications priorities: - * First `init=true` and `load=true` commands are executed (if both are present, INIT prevails on LOAD) - * Then parameters included in json are taken in account - * Finally `erase=true` and `save=true` commands are executed (if both are present, ERASE prevails on SAVE) - */ - if (BTdata.containsKey("init") && BTdata["init"].as()) { - // Restore the default (initial) configuration - BTConfig_init(); - } else if (BTdata.containsKey("load") && BTdata["load"].as()) { - // Load the saved configuration, if not initialised - BTConfig_load(); - } - - // Load config from json if available - BTConfig_fromJson(BTdata); - - } else if (cmpToMainTopic(topicOri, subjectMQTTtoBT)) { - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { - KnownBTActions(BTdata); - XtoBTAction(BTdata); - xSemaphoreGive(semaphoreBLEOperation); - } else { - Log.error(F("BLE busy - BTActions not sent" CR)); - gatewayState = GatewayState::ERROR; - } - } else if (strstr(topicOri, subjectTrackerSync) != NULL) { - if (BTdata.containsKey("gatewayid") && BTdata.containsKey("trackerid") && BTdata["gatewayid"] != gateway_name) { - BLEdevice* device = getDeviceByMac(BTdata["trackerid"].as()); - if (device != &NO_BT_DEVICE_FOUND && device->lastUpdate != 0) { - device->lastUpdate = 0; - Log.notice(F("Tracker %s disassociated by gateway %s" CR), BTdata["trackerid"].as(), BTdata["gatewayid"].as()); - } - } - } -} -#endif +/* + OpenMQTTGateway - ESP8266 or Arduino program for home automation + + Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker + Send and receiving command by MQTT + + This gateway enables to: + - publish MQTT data to a topic related to BLE devices data + + Copyright: (c)Florian ROBERT + + This file is part of OpenMQTTGateway. + + OpenMQTTGateway is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenMQTTGateway is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Thanks to wolass https://github.com/wolass for suggesting me HM 10 and dinosd https://github.com/dinosd/BLE_PROXIMITY for inspiring me how to implement the gateway +*/ +#include "User_config.h" + +#ifdef ZgatewayBT +# include "TheengsCommon.h" + +SemaphoreHandle_t semaphoreCreateOrUpdateDevice; +SemaphoreHandle_t semaphoreBLEOperation; +QueueHandle_t BLEQueue; +unsigned long scanCount = 0; +# include +# include +# include +# include +# include +# include + +# include + +# include "TheengsCommon.h" +# include "config_mqttDiscovery.h" +# include "gatewayBLEConnect.h" +# include "soc/timer_group_reg.h" +# include "soc/timer_group_struct.h" + +using namespace std; + +// Global struct to store live BT configuration data +BTConfig_s BTConfig; + +# if BLEDecoder +# include +# if BLEDecryptor +# include "mbedtls/ccm.h" +# include "mbedtls/aes.h" +# endif +TheengsDecoder decoder; +# endif + +static TaskHandle_t xCoreTaskHandle; +static TaskHandle_t xProcBLETaskHandle; + +struct decompose { + int start; + int len; + bool reverse; +}; + +vector BLEactions; + +vector devices; +int newDevices = 0; + +static BLEdevice NO_BT_DEVICE_FOUND = { + {0}, + 0, + false, + false, + false, + false, + (char)UNKWNON_MODEL, + 0, +}; +static bool oneWhite = false; + +extern bool BTProcessLock; +extern int queueLength; + +void setupBTTasksAndBLE(); +bool checkIfIsTracker(char ch); +void hass_presence(JsonObject& HomePresence); +void BTforceScan(); + +void BTConfig_init() { + BTConfig.bleConnect = AttemptBLEConnect; + BTConfig.BLEinterval = TimeBtwRead; + BTConfig.adaptiveScan = AdaptiveBLEScan; + BTConfig.intervalActiveScan = TimeBtwActive; + BTConfig.intervalConnect = TimeBtwConnect; + BTConfig.scanDuration = Scan_duration; + BTConfig.pubOnlySensors = PublishOnlySensors; + BTConfig.pubRandomMACs = PublishRandomMACs; + BTConfig.presenceEnable = HassPresence; + BTConfig.presenceTopic = subjectHomePresence; + BTConfig.presenceUseBeaconUuid = useBeaconUuidForPresence; + BTConfig.minRssi = MinimumRSSI; + BTConfig.extDecoderEnable = UseExtDecoder; + BTConfig.extDecoderTopic = MQTTDecodeTopic; + BTConfig.filterConnectable = BLE_FILTER_CONNECTABLE; + BTConfig.pubAdvData = pubBLEAdvData; + BTConfig.pubBeaconUuidForTopic = useBeaconUuidForTopic; + BTConfig.ignoreWBlist = false; + BTConfig.presenceAwayTimer = PresenceAwayTimer; + BTConfig.movingTimer = MovingTimer; + BTConfig.forcePassiveScan = false; + BTConfig.enabled = EnableBT; +} + +unsigned long timeBetweenConnect = 0; +unsigned long timeBetweenActive = 0; + +String stateBTMeasures(bool start) { + StaticJsonDocument jsonBuffer; + JsonObject jo = jsonBuffer.to(); + jo["bleconnect"] = BTConfig.bleConnect; + jo["interval"] = BTConfig.BLEinterval; + jo["adaptivescan"] = BTConfig.adaptiveScan; + jo["intervalacts"] = BTConfig.intervalActiveScan; + jo["intervalcnct"] = BTConfig.intervalConnect; + jo["scanduration"] = BTConfig.scanDuration; + jo["hasspresence"] = BTConfig.presenceEnable; + jo["prestopic"] = BTConfig.presenceTopic; + jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; + jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value + jo["extDecoderEnable"] = BTConfig.extDecoderEnable; + jo["extDecoderTopic"] = BTConfig.extDecoderTopic; + jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; + jo["ignoreWBlist"] = BTConfig.ignoreWBlist; + jo["forcepscn"] = BTConfig.forcePassiveScan; + jo["tskstck"] = uxTaskGetStackHighWaterMark(xProcBLETaskHandle); + jo["crstck"] = uxTaskGetStackHighWaterMark(xCoreTaskHandle); + jo["enabled"] = BTConfig.enabled; + jo["scnct"] = scanCount; +# if BLEDecoder + jo["onlysensors"] = BTConfig.pubOnlySensors; + jo["randommacs"] = BTConfig.pubRandomMACs; + jo["filterConnectable"] = BTConfig.filterConnectable; + jo["pubadvdata"] = BTConfig.pubAdvData; + jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; + jo["movingtimer"] = BTConfig.movingTimer; +# endif + + if (start) { + Log.notice(F("BT sys: ")); + serializeJsonPretty(jsonBuffer, Serial); + Serial.println(); + return ""; // Do not try to erase/write/send config at startup + } + String output; + serializeJson(jo, output); + jo["origin"] = subjectBTtoMQTT; + enqueueJsonObject(jo, QueueSemaphoreTimeOutTask); + return (output); +} + +void BTConfig_fromJson(JsonObject& BTdata, bool startup = false) { + // Attempts to connect to eligible devices or not + Config_update(BTdata, "bleconnect", BTConfig.bleConnect); + // Identify AdaptiveScan deactivation to pass to continuous mode or activation to come back to default settings + if (startup == false) { + if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == false && BTConfig.presenceEnable == true) { + BTdata["adaptivescan"] = true; + } else if (BTdata.containsKey("hasspresence") && BTdata["hasspresence"] == true && BTConfig.presenceEnable == false) { + BTdata["adaptivescan"] = false; + } + + if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == false && BTConfig.adaptiveScan == true) { + BTdata["interval"] = MinTimeBtwScan; + BTdata["intervalacts"] = MinTimeBtwScan; + BTdata["scanduration"] = MinScanDuration; + } else if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == true && BTConfig.adaptiveScan == false) { + BTdata["interval"] = TimeBtwRead; + BTdata["intervalacts"] = TimeBtwActive; + BTdata["scanduration"] = Scan_duration; + } + // Identify if the gateway is enabled or not and stop start accordingly + if (BTdata.containsKey("enabled") && BTdata["enabled"] == false && BTConfig.enabled == true) { + // Stop the gateway but without deinit to enable a future BT restart + stopProcessing(false); + } else if (BTdata.containsKey("enabled") && BTdata["enabled"] == true && BTConfig.enabled == false) { + BTProcessLock = false; + setupBTTasksAndBLE(); + } + } + // Home Assistant presence message + Config_update(BTdata, "hasspresence", BTConfig.presenceEnable); + // Time before before active scan + // Scan interval set - and avoid intervalacts to be lower than interval + if (BTdata.containsKey("interval") && BTdata["interval"] != 0) { + BTConfig.adaptiveScan = false; + Config_update(BTdata, "interval", BTConfig.BLEinterval); + if (BTConfig.intervalActiveScan < BTConfig.BLEinterval) { + Config_update(BTdata, "interval", BTConfig.intervalActiveScan); + } + } + // Define if the scan is adaptive or not - and avoid intervalacts to be lower than interval + if (BTdata.containsKey("intervalacts") && BTdata["intervalacts"] < BTConfig.BLEinterval) { + BTConfig.adaptiveScan = false; + // Config_update(BTdata, "interval", BTConfig.intervalActiveScan); + BTConfig.intervalActiveScan = BTConfig.BLEinterval; + } else { + Config_update(BTdata, "intervalacts", BTConfig.intervalActiveScan); + } + // Adaptive scan set + Config_update(BTdata, "adaptivescan", BTConfig.adaptiveScan); + // Time before a connect set + Config_update(BTdata, "intervalcnct", BTConfig.intervalConnect); + // publish all BLE devices discovered or only the identified sensors (like temperature sensors) + Config_update(BTdata, "scanduration", BTConfig.scanDuration); + // define the duration for a scan; in milliseconds + Config_update(BTdata, "onlysensors", BTConfig.pubOnlySensors); + // publish devices which randomly change their MAC addresses + Config_update(BTdata, "randommacs", BTConfig.pubRandomMACs); + // Home Assistant presence message topic + Config_update(BTdata, "prestopic", BTConfig.presenceTopic); + // Home Assistant presence message use iBeacon UUID + Config_update(BTdata, "presuseuuid", BTConfig.presenceUseBeaconUuid); + // Timer to trigger a device state as offline if not seen + Config_update(BTdata, "presenceawaytimer", BTConfig.presenceAwayTimer); + // Timer to trigger a device state as offline if not seen + Config_update(BTdata, "movingtimer", BTConfig.movingTimer); + // Force passive scan + Config_update(BTdata, "forcepscn", BTConfig.forcePassiveScan); + // MinRSSI set + Config_update(BTdata, "minrssi", BTConfig.minRssi); + // Send undecoded device data + Config_update(BTdata, "extDecoderEnable", BTConfig.extDecoderEnable); + // Topic to send undecoded device data + Config_update(BTdata, "extDecoderTopic", BTConfig.extDecoderTopic); + // Sets whether to filter publishing + Config_update(BTdata, "filterConnectable", BTConfig.filterConnectable); + // Publish advertisement data + Config_update(BTdata, "pubadvdata", BTConfig.pubAdvData); + // Use iBeacon UUID as topic, instead of sender (random) MAC address + Config_update(BTdata, "pubuuid4topic", BTConfig.pubBeaconUuidForTopic); + // Disable Whitelist & Blacklist + Config_update(BTdata, "ignoreWBlist", (BTConfig.ignoreWBlist)); + // Enable or disable the BT gateway + Config_update(BTdata, "enabled", BTConfig.enabled); + + stateBTMeasures(startup); + + if (BTdata.containsKey("erase") && BTdata["erase"].as()) { + // Erase config from NVS (non-volatile storage) + preferences.begin(Gateway_Short_Name, false); + if (preferences.isKey("BTConfig")) { + int result = preferences.remove("BTConfig"); + Log.notice(F("BT config erase result: %d" CR), result); + preferences.end(); + return; // Erase prevails on save, so skipping save + } else { + preferences.end(); + Log.notice(F("BT config not found" CR)); + } + } + + if (BTdata.containsKey("save") && BTdata["save"].as()) { + StaticJsonDocument jsonBuffer; + JsonObject jo = jsonBuffer.to(); + jo["bleconnect"] = BTConfig.bleConnect; + jo["interval"] = BTConfig.BLEinterval; + jo["adaptivescan"] = BTConfig.adaptiveScan; + jo["intervalacts"] = BTConfig.intervalActiveScan; + jo["intervalcnct"] = BTConfig.intervalConnect; + jo["scanduration"] = BTConfig.scanDuration; + jo["onlysensors"] = BTConfig.pubOnlySensors; + jo["randommacs"] = BTConfig.pubRandomMACs; + jo["hasspresence"] = BTConfig.presenceEnable; + jo["prestopic"] = BTConfig.presenceTopic; + jo["presuseuuid"] = BTConfig.presenceUseBeaconUuid; + jo["minrssi"] = -abs(BTConfig.minRssi); // Always export as negative value + jo["extDecoderEnable"] = BTConfig.extDecoderEnable; + jo["extDecoderTopic"] = BTConfig.extDecoderTopic; + jo["filterConnectable"] = BTConfig.filterConnectable; + jo["pubadvdata"] = BTConfig.pubAdvData; + jo["pubuuid4topic"] = BTConfig.pubBeaconUuidForTopic; + jo["ignoreWBlist"] = BTConfig.ignoreWBlist; + jo["presenceawaytimer"] = BTConfig.presenceAwayTimer; + jo["movingtimer"] = BTConfig.movingTimer; + jo["forcepscn"] = BTConfig.forcePassiveScan; + jo["enabled"] = BTConfig.enabled; + // Save config into NVS (non-volatile storage) + String conf = ""; + serializeJson(jsonBuffer, conf); + preferences.begin(Gateway_Short_Name, false); + int result = preferences.putString("BTConfig", conf); + preferences.end(); + Log.notice(F("BT config save: %s, result: %d" CR), conf.c_str(), result); + } +} + +void BTConfig_load() { + StaticJsonDocument jsonBuffer; + preferences.begin(Gateway_Short_Name, true); + if (preferences.isKey("BTConfig")) { + auto error = deserializeJson(jsonBuffer, preferences.getString("BTConfig", "{}")); + preferences.end(); + Log.notice(F("BT config loaded" CR)); + if (error) { + Log.error(F("BT config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity()); + return; + } + if (jsonBuffer.isNull()) { + Log.warning(F("BT config is null" CR)); + return; + } + JsonObject jo = jsonBuffer.as(); + BTConfig_fromJson(jo, true); // Never send MQTT message with config + Log.notice(F("BT config loaded" CR)); + } else { + preferences.end(); + Log.notice(F("BT config not found" CR)); + } +} + +void PublishDeviceData(JsonObject& BLEdata); + +atomic_int forceBTScan; + +void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type = 0, const char* name = ""); + +BLEdevice* getDeviceByMac(const char* mac); // Declared here to avoid pre-compilation issue (misplaced auto declaration by pio) +BLEdevice* getDeviceByMac(const char* mac) { + Log.trace(F("getDeviceByMac %s" CR), mac); + + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + if ((strcmp((*it)->macAdr, mac) == 0)) { + return *it; + } + } + return &NO_BT_DEVICE_FOUND; +} + +bool updateWorB(JsonObject& BTdata, bool isWhite) { + Log.trace(F("update WorB" CR)); + const char* jsonKey = isWhite ? "white-list" : "black-list"; + + int size = BTdata[jsonKey].size(); + if (size == 0) + return false; + + for (int i = 0; i < size; i++) { + const char* mac = BTdata[jsonKey][i]; + createOrUpdateDevice(mac, (isWhite ? device_flags_isWhiteL : device_flags_isBlackL), + UNKWNON_MODEL); + } + + return true; +} + +void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_type, const char* name) { + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(30000)) == pdFALSE) { + Log.error(F("Semaphore NOT taken" CR)); + return; + } + BLEdevice* device = getDeviceByMac(mac); + if (device == &NO_BT_DEVICE_FOUND) { + Log.trace(F("add %s" CR), mac); + //new device + device = new BLEdevice(); + strcpy(device->macAdr, mac); + device->isDisc = flags & device_flags_isDisc; + device->isWhtL = flags & device_flags_isWhiteL; + device->isBlkL = flags & device_flags_isBlackL; + device->connect = flags & device_flags_connect; + device->macType = mac_type; + // Check name length + if (strlen(name) > 20) { + Log.warning(F("Name too long, truncating" CR)); + strncpy(device->name, name, 20); + device->name[19] = '\0'; + } else { + strcpy(device->name, name); + } + device->sensorModel_id = model; + device->lastUpdate = millis(); + devices.push_back(device); + newDevices++; + } else { + Log.trace(F("update %s" CR), mac); + device->lastUpdate = millis(); + device->macType = mac_type; + + if (flags & device_flags_isDisc) { + device->isDisc = true; + } + + if (flags & device_flags_connect) { + device->connect = true; + } + + if (model != UNKWNON_MODEL && device->sensorModel_id == UNKWNON_MODEL) { + newDevices++; + device->isDisc = false; + device->sensorModel_id = model; + } + + // If a device has been added to the white-list, flag it so it can be auto-detected + if (!device->isWhtL && flags & device_flags_isWhiteL) { + newDevices++; + } + if (flags & device_flags_isWhiteL || flags & device_flags_isBlackL) { + device->isWhtL = flags & device_flags_isWhiteL; + device->isBlkL = flags & device_flags_isBlackL; + } + } + + // update oneWhite flag + oneWhite = oneWhite || device->isWhtL; + + xSemaphoreGive(semaphoreCreateOrUpdateDevice); +} + +void updateDevicesStatus() { + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + BLEdevice* p = *it; + unsigned long now = millis(); + // Check for tracker status + bool isTracker = false; +# if BLEDecoder + std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); + if (tag.length() >= 4) { + isTracker = checkIfIsTracker(tag[3]); + } + // Device tracker devices + if (isTracker) { // We apply the offline status only for tracking device, can be extended further to all the devices + if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.presenceAwayTimer) && (now > BTConfig.presenceAwayTimer)) && + (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = p->macAdr; + BLEdata["state"] = "offline"; + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + // We set the lastUpdate to 0 to avoid replublishing the offline state + p->lastUpdate = 0; + } + } + // Moving detection devices (devices with an accelerometer) + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BC08) { + if ((p->lastUpdate != 0) && (p->lastUpdate < (now - BTConfig.movingTimer) && (now > BTConfig.movingTimer)) && + (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(p)) && !isBlack(p)))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC)) { + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = p->macAdr; + BLEdata["state"] = "offline"; + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + // We set the lastUpdate to 0 to avoid replublishing the offline state + p->lastUpdate = 0; + } + } +# endif + } +} + +void dumpDevices() { +# if LOG_LEVEL > LOG_LEVEL_NOTICE + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + BLEdevice* p = *it; + Log.trace(F("macAdr %s" CR), p->macAdr); + Log.trace(F("macType %d" CR), p->macType); + Log.trace(F("isDisc %d" CR), p->isDisc); + Log.trace(F("isWhtL %d" CR), p->isWhtL); + Log.trace(F("isBlkL %d" CR), p->isBlkL); + Log.trace(F("connect %d" CR), p->connect); + Log.trace(F("sensorModel_id %d" CR), p->sensorModel_id); + Log.trace(F("LastUpdate %u" CR), p->lastUpdate); + } +# endif +} + +void strupp(char* beg) { + while ((*beg = toupper(*beg))) + ++beg; +} + +# ifdef ZmqttDiscovery +void DT24Discovery(const char* mac, const char* sensorModel_id) { +# define DT24parametersCount 7 + Log.trace(F("DT24Discovery" CR)); + const char* DT24sensor[DT24parametersCount][9] = { + {"sensor", "volt", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "amp", mac, "current", jsonCurrent, "", "", "A", stateClassMeasurement}, + {"sensor", "watt", mac, "power", jsonPower, "", "", "W", stateClassMeasurement}, + {"sensor", "watt-hour", mac, "power", jsonEnergy, "", "", "kWh", stateClassMeasurement}, + {"sensor", "price", mac, "", jsonMsg, "", "", "", stateClassNone}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"binary_sensor", "inUse", mac, "power", jsonInuse, "", "", "", stateClassNone} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, DT24sensor, DT24parametersCount, "DT24", "ATorch", sensorModel_id); +} + +void BM2Discovery(const char* mac, const char* sensorModel_id) { +# define BM2parametersCount 2 + Log.trace(F("BM2Discovery" CR)); + const char* BM2sensor[BM2parametersCount][9] = { + {"sensor", "volt", mac, "voltage", jsonVoltBM2, "", "", "V", stateClassMeasurement}, // We use a json definition that retrieve only data from the BM2 decoder, as this sensor also advertize volt as an iBeacon + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, BM2sensor, BM2parametersCount, "BM2", "Generic", sensorModel_id); +} + +void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) { +# define LYWSD03MMCparametersCount 4 + Log.trace(F("LYWSD03MMCDiscovery" CR)); + const char* LYWSD03MMCsensor[LYWSD03MMCparametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, LYWSD03MMCsensor, LYWSD03MMCparametersCount, "LYWSD03MMC", "Xiaomi", sensorModel); +} + +void MHO_C401Discovery(const char* mac, const char* sensorModel) { +# define MHO_C401parametersCount 4 + Log.trace(F("MHO_C401Discovery" CR)); + const char* MHO_C401sensor[MHO_C401parametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, MHO_C401sensor, MHO_C401parametersCount, "MHO_C401", "Xiaomi", sensorModel); +} + +void HHCCJCY01HHCCDiscovery(const char* mac, const char* sensorModel) { +# define HHCCJCY01HHCCparametersCount 5 + Log.trace(F("HHCCJCY01HHCCDiscovery" CR)); + const char* HHCCJCY01HHCCsensor[HHCCJCY01HHCCparametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "lux", mac, "illuminance", jsonLux, "", "", "lx", stateClassMeasurement}, + {"sensor", "fer", mac, "", jsonFer, "", "", "µS/cm", stateClassMeasurement}, + {"sensor", "moi", mac, "", jsonMoi, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, HHCCJCY01HHCCsensor, HHCCJCY01HHCCparametersCount, "HHCCJCY01HHCC", "Xiaomi", sensorModel); +} + +void XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel) { +# define XMWSDJ04MMCparametersCount 4 + Log.trace(F("XMWSDJ04MMCDiscovery" CR)); + const char* XMWSDJ04MMCsensor[XMWSDJ04MMCparametersCount][9] = { + {"sensor", "batt", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "volt", mac, "", jsonVolt, "", "", "V", stateClassMeasurement}, + {"sensor", "temp", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "hum", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, XMWSDJ04MMCsensor, XMWSDJ04MMCparametersCount, "XMWSDJ04MMC", "Xiaomi", sensorModel); +} + +# else +void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) {} +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 XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel_id) {} +# endif + +/* + Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleScan.cpp + Ported to Arduino ESP32 by Evandro Copercini + */ +// core task implementation thanks to https://techtutorialsx.com/2017/05/09/esp32-running-code-on-a-specific-core/ + +//core on which the BLE detection task will run +static int taskCore = 0; + +class ScanCallbacks : public NimBLEScanCallbacks { + void onResult(const NimBLEAdvertisedDevice* advertisedDevice) { + NimBLEAdvertisedDevice* ad = new NimBLEAdvertisedDevice(*advertisedDevice); + if (xQueueSend(BLEQueue, &ad, 0) != pdTRUE) { + Log.error(F("BLEQueue full" CR)); + delete (ad); + } + } +} scanCallbacks; + +std::string convertServiceData(std::string deviceServiceData) { + int serviceDataLength = (int)deviceServiceData.length(); + char spr[2 * serviceDataLength + 1]; + for (int i = 0; i < serviceDataLength; i++) sprintf(spr + 2 * i, "%.2x", (unsigned char)deviceServiceData[i]); + spr[2 * serviceDataLength] = 0; + Log.trace(F("Converted service data (%d) to %s" CR), serviceDataLength, spr); + return spr; +} + +bool checkIfIsTracker(char ch) { + uint8_t data = 0; + if (ch >= '0' && ch <= '9') + data = ch - '0'; + else if (ch >= 'a' && ch <= 'f') + data = 10 + (ch - 'a'); + + if (((data >> 3) & 0x01) == 1) { + Log.trace(F("Is Device Tracker" CR)); + return true; + } else { + return false; + } +} + +void procBLETask(void* pvParameters) { + BLEAdvertisedDevice* advertisedDevice = nullptr; + + for (;;) { + xQueueReceive(BLEQueue, &advertisedDevice, portMAX_DELAY); + // Feed the watchdog + //esp_task_wdt_reset(); + if (!BTProcessLock) { + Log.trace(F("Creating BLE buffer" CR)); + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = advertisedDevice->getAddress().toString(); + BLEdata["mac_type"] = advertisedDevice->getAddress().getType(); + BLEdata["adv_type"] = advertisedDevice->getAdvType(); + Log.notice(F("BT Device detected: %s" CR), BLEdata["id"].as()); + BLEdevice* device = getDeviceByMac(BLEdata["id"].as()); + + if (BTConfig.filterConnectable && device->connect) { + Log.notice(F("Filtered connectable device" CR)); + delete (advertisedDevice); + continue; + } + + if (BTConfig.ignoreWBlist || ((!oneWhite || isWhite(device)) && !isBlack(device))) { // Only if WBlist is disabled OR ((no white MAC OR this MAC is white) AND not a black listed MAC) + if (advertisedDevice->haveName()) + BLEdata["name"] = (char*)advertisedDevice->getName().c_str(); + if (advertisedDevice->haveManufacturerData()) { + BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString((uint8_t*)advertisedDevice->getManufacturerData().data(), + advertisedDevice->getManufacturerData().length()); + } + BLEdata["rssi"] = (int)advertisedDevice->getRSSI(); + if (advertisedDevice->haveTXPower()) + BLEdata["txpower"] = (int8_t)advertisedDevice->getTXPower(); + if (BTConfig.presenceEnable) { + hass_presence(BLEdata); // with either only sensors or not we can use it for home assistant room presence component + } + if (advertisedDevice->haveServiceData()) { + int serviceDataCount = advertisedDevice->getServiceDataCount(); + Log.trace(F("Get services data number: %d" CR), serviceDataCount); + for (int j = 0; j < serviceDataCount; j++) { + StaticJsonDocument BLEdataBufferTemp; + JsonObject BLEdataTemp = BLEdataBufferTemp.to(); + BLEdataBufferTemp = BLEdataBuffer; + std::string service_data = convertServiceData(advertisedDevice->getServiceData(j)); + Log.trace(F("Service data: %s" CR), service_data.c_str()); + std::string serviceDatauuid = advertisedDevice->getServiceDataUUID(j).toString(); + Log.trace(F("Service data UUID: %s" CR), (char*)serviceDatauuid.c_str()); + BLEdataTemp["servicedata"] = (char*)service_data.c_str(); + BLEdataTemp["servicedatauuid"] = (char*)serviceDatauuid.c_str(); + PublishDeviceData(BLEdataTemp); + } + } else { + PublishDeviceData(BLEdata); + } + } else { + Log.trace(F("Filtered MAC device" CR)); + } + updateDevicesStatus(); + } + delete (advertisedDevice); + vTaskDelay(10); + } +} + +/** + * BLEscan used to retrieve BLE advertized data from devices without connection + */ +void BLEscan() { + // Don't start the next scan until processing of previous results is complete. + while (uxQueueMessagesWaiting(BLEQueue) || queueLength != 0) { // the criteria on queueLength could be adjusted to parallelize the scan and the queue processing + delay(1); // Wait for queue to empty, a yield here instead of the delay cause the WDT to trigger + } + Log.notice(F("Scan begin" CR)); + BLEScan* pBLEScan = BLEDevice::getScan(); + pBLEScan->setScanCallbacks(&scanCallbacks); + if ((millis() > (timeBetweenActive + BTConfig.intervalActiveScan) || BTConfig.intervalActiveScan == BTConfig.BLEinterval) && !BTConfig.forcePassiveScan) { + pBLEScan->setActiveScan(true); + timeBetweenActive = millis(); + } else { + pBLEScan->setActiveScan(false); + } + pBLEScan->setInterval(BLEScanInterval); + pBLEScan->setWindow(BLEScanWindow); + NimBLEScanResults foundDevices = pBLEScan->getResults(BTConfig.scanDuration, false); + if (foundDevices.getCount()) + scanCount++; + Log.notice(F("Found %d devices, scan number %d end" CR), foundDevices.getCount(), scanCount); + Log.trace(F("Process BLE stack free: %u" CR), uxTaskGetStackHighWaterMark(xProcBLETaskHandle)); +} + +/** + * Connect to BLE devices and initiate the callbacks with a service/characteristic request + */ +# if BLEDecoder +void BLEconnect() { + if (!BTProcessLock) { + Log.notice(F("BLE Connect begin" CR)); + do { + for (vector::iterator it = devices.begin(); it != devices.end(); ++it) { + BLEdevice* p = *it; + if (p->connect) { + Log.trace(F("Model to connect found: %s" CR), p->macAdr); + NimBLEAddress addr((const char*)p->macAdr, p->macType); + if (p->sensorModel_id == BLEconectable::id::LYWSD03MMC || + p->sensorModel_id == BLEconectable::id::MHO_C401) { + LYWSD03MMC_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + BLEclient.publishData(); + } else if (p->sensorModel_id == BLEconectable::id::DT24_BLE) { + DT24_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + BLEclient.publishData(); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { + BM2_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); + BLEclient.publishData(); + } else if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { + XMWSDJ04MMC_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + BLEclient.publishData(); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1) { + SBS1_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT) { + SBBT_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU) { + SBCU_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + } else { + GENERIC_connect BLEclient(addr); + if (BLEclient.processActions(BLEactions)) { + // If we don't regularly connect to this, disable connections so advertisements + // won't be filtered if BLE_FILTER_CONNECTABLE is set. + p->connect = false; + } + } + if (BLEactions.size() > 0) { + std::vector swap; + for (auto& it : BLEactions) { + if (!it.complete && --it.ttl) { + swap.push_back(it); + } else if (it.addr == NimBLEAddress(p->macAdr, p->macType)) { + if (p->sensorModel_id != BLEconectable::id::DT24_BLE && + 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 != BLEconectable::id::MHO_C401 && + p->sensorModel_id != BLEconectable::id::XMWSDJ04MMC) { + // if irregulary connected to and connection failed clear the connect flag. + p->connect = false; + } + } + } + std::swap(BLEactions, swap); + } + } + } + } while (BLEactions.size() > 0); + Log.notice(F("BLE Connect end" CR)); + } +} +# else +void BLEconnect() {} +# endif + +void stopProcessing(bool deinit) { + if (BTConfig.enabled) { + BTProcessLock = true; + // We stop the scan + Log.notice(F("Stopping BLE scan" CR)); + BLEScan* pBLEScan = BLEDevice::getScan(); + if (pBLEScan->isScanning()) { + pBLEScan->stop(); + } + + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { + Log.notice(F("Stopping BLE tasks" CR)); + //Suspending, deleting tasks and stopping BT to free memory + vTaskSuspend(xCoreTaskHandle); + vTaskDelete(xCoreTaskHandle); + vTaskSuspend(xProcBLETaskHandle); + vTaskDelete(xProcBLETaskHandle); + xSemaphoreGive(semaphoreBLEOperation); + } + // Using deinit to free memory, should only be used if we are going to restart the gateway + if (deinit) + BLEDevice::deinit(true); + } + Log.notice(F("BLE gateway stopped, free heap: %d" CR), ESP.getFreeHeap()); +} + +void coreTask(void* pvParameters) { + while (true) { + if (!BTProcessLock) { + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(30000)) == pdTRUE) { + BLEscan(); + // Launching a connect every TimeBtwConnect + if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { + timeBetweenConnect = millis(); + BLEconnect(); + } + //dumpDevices(); + Log.trace(F("CoreTask stack free: %u" CR), uxTaskGetStackHighWaterMark(xCoreTaskHandle)); + xSemaphoreGive(semaphoreBLEOperation); + } else { + Log.error(F("Failed to start scan - BLE busy" CR)); + } + if (SYSConfig.powerMode > 0) { + int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); // is this enough, it will wait the full deepsleep... + if (scan == 1) BTforceScan(); + ready_to_sleep = true; + } else { + for (int interval = BTConfig.BLEinterval, waitms; interval > 0; interval -= waitms) { + int scan = atomic_exchange_explicit(&forceBTScan, 0, ::memory_order_seq_cst); + if (scan == 1) BTforceScan(); // should we break after this? + delay(waitms = interval > 100 ? 100 : interval); // 100ms + } + } + } + delay(1); + } +} + +void setupBTTasksAndBLE() { +# ifdef CONFIG_BTDM_BLE_SCAN_DUPL + BLEDevice::setScanDuplicateCacheSize(BLEScanDuplicateCacheSize); +# endif + BLEDevice::init(""); + xTaskCreateUniversal( + procBLETask, /* Function to implement the task */ + "procBLETask", /* Name of the task */ +# if defined(USE_ESP_IDF) || defined(USE_BLUFI) + 14500, +# else + 9500, /* Stack size in bytes */ +# endif + NULL, /* Task input parameter */ + 2, /* Priority of the task (set higher than core task) */ + &xProcBLETaskHandle, /* Task handle. */ + 1); /* Core where the task should run */ + + // we setup a task with priority one to avoid conflict with other gateways + xTaskCreateUniversal( + coreTask, /* Function to implement the task */ + "coreTask", /* Name of the task */ + 5120, /* Stack size in bytes */ + NULL, /* Task input parameter */ + 1, /* Priority of the task */ + &xCoreTaskHandle, /* Task handle. */ + taskCore); /* Core where the task should run */ +} + +void setupBT() { + BTConfig_init(); + BTConfig_load(); + Log.notice(F("BLE scans interval: %d" CR), BTConfig.BLEinterval); + Log.notice(F("BLE connects interval: %d" CR), BTConfig.intervalConnect); + Log.notice(F("BLE scan duration: %d" CR), BTConfig.scanDuration); + Log.notice(F("Publishing only BLE sensors: %T" CR), BTConfig.pubOnlySensors); + Log.notice(F("Publishing random MAC devices: %T" CR), BTConfig.pubRandomMACs); + Log.notice(F("Adaptive BLE scan: %T" CR), BTConfig.adaptiveScan); + Log.notice(F("Active BLE scan interval: %d" CR), BTConfig.intervalActiveScan); + Log.notice(F("minrssi: %d" CR), -abs(BTConfig.minRssi)); + Log.notice(F("Presence Away Timer: %d" CR), BTConfig.presenceAwayTimer); + Log.notice(F("Moving Timer: %d" CR), BTConfig.movingTimer); + Log.notice(F("Force passive scan: %T" CR), BTConfig.forcePassiveScan); + Log.notice(F("Enabled BLE: %T" CR), BTConfig.enabled); + + atomic_init(&forceBTScan, 0); // in theory, we don't need this + + semaphoreCreateOrUpdateDevice = xSemaphoreCreateBinary(); + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + + semaphoreBLEOperation = xSemaphoreCreateBinary(); + xSemaphoreGive(semaphoreBLEOperation); + + BLEQueue = xQueueCreate(QueueSize, sizeof(NimBLEAdvertisedDevice*)); + if (BTConfig.enabled) { + setupBTTasksAndBLE(); + Log.notice(F("gatewayBT multicore ESP32 setup done" CR)); + } else { + Log.notice(F("gatewayBT multicore ESP32 setup disabled" CR)); + } +} + +boolean valid_service_data(const char* data, int size) { + for (int i = 0; i < size; ++i) { + if (data[i] != 48) // 48 correspond to 0 in ASCII table + return true; + } + return false; +} + +# if defined(ZmqttDiscovery) && BLEDecoder == true +// This function always should be called from the main core as it generates direct mqtt messages +// When overrideDiscovery=true, we publish discovery messages of known devices (even if no new) +void launchBTDiscovery(bool overrideDiscovery) { + if (!overrideDiscovery && newDevices == 0) + return; + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdFALSE) { + Log.error(F("Semaphore NOT taken" CR)); + return; + } + newDevices = 0; + vector localDevices = devices; + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + for (vector::iterator it = localDevices.begin(); it != localDevices.end(); ++it) { + BLEdevice* p = *it; + Log.trace(F("Device mac %s" CR), p->macAdr); + // Do not launch discovery for the devices already discovered (unless we have overrideDiscovery) or that are not unique by their MAC Address (iBeacon, GAEN and Microsoft CDP) + if (overrideDiscovery || !isDiscovered(p)) { + String macWOdots = String(p->macAdr); + macWOdots.replace(":", ""); + if (p->sensorModel_id >= 0) { + Log.trace(F("Looking for Model_id: %d" CR), p->sensorModel_id); + std::string properties = decoder.getTheengProperties(p->sensorModel_id); + Log.trace(F("properties: %s" CR), properties.c_str()); + std::string brand = decoder.getTheengAttribute(p->sensorModel_id, "brand"); + std::string model = decoder.getTheengAttribute(p->sensorModel_id, "model"); +# if ForceDeviceName + if (p->name[0] != '\0') { + model = p->name; + } +# endif + std::string model_id = decoder.getTheengAttribute(p->sensorModel_id, "model_id"); + + // Check for tracker status + bool isTracker = false; + std::string tag = decoder.getTheengAttribute(p->sensorModel_id, "tag"); + if (tag.length() >= 4) { + isTracker = checkIfIsTracker(tag[3]); + } + + String discovery_topic = String(subjectBTtoMQTT) + "/" + macWOdots; + 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 + if (isTracker) { + String tracker_name = String(model_id.c_str()) + "-tracker"; + String tracker_id = macWOdots + "-tracker"; + createDiscovery("device_tracker", + discovery_topic.c_str(), tracker_name.c_str(), 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 == TheengsDecoder::BLE_ID_NUM::BC08) { + String sensor_name = String(model_id.c_str()) + "-moving"; + String sensor_id = macWOdots + "-moving"; + createDiscovery("binary_sensor", + discovery_topic.c_str(), sensor_name.c_str(), sensor_id.c_str(), + will_Topic, "moving", "{% if value_json.get('accx') -%}on{%- else -%}off{%- endif %}", + "on", "off", "", + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } + if (!properties.empty()) { + StaticJsonDocument jsonBuffer; + auto error = deserializeJson(jsonBuffer, properties); + if (error) { + if (jsonBuffer.overflowed()) { + // This should not happen if JSON_MSG_BUFFER is large enough for + // the Theengs json properties + Log.error(F("JSON deserialization of Theengs properties overflowed (error %s), buffer capacity: %u. Program might crash. Properties json: %s" CR), + error.c_str(), jsonBuffer.capacity(), properties.c_str()); + } else { + Log.error(F("JSON deserialization of Theengs properties errored: %" CR), + error.c_str()); + } + } + for (JsonPair prop : jsonBuffer["properties"].as()) { + Log.trace(F("Key: %s"), prop.key().c_str()); + Log.trace(F("Unit: %s"), prop.value()["unit"].as()); + Log.trace(F("Name: %s"), prop.value()["name"].as()); + String entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); + String unique_id = macWOdots + "-" + String(prop.key().c_str()); + String value_template = "{{ value_json." + String(prop.key().c_str()) + " | is_defined }}"; + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1 && strcmp(prop.key().c_str(), "state") == 0) { + String payload_on = "{\"model_id\":\"X1\",\"cmd\":\"on\",\"id\":\"" + String(p->macAdr) + "\"}"; + String payload_off = "{\"model_id\":\"X1\",\"cmd\":\"off\",\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("switch", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "switch", value_template.c_str(), + payload_on.c_str(), payload_off.c_str(), "", 0, + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone, "off", "on"); + unique_id = macWOdots + "-press"; + entity_name = String(model_id.c_str()) + "-press"; + String payload_press = "{\"model_id\":\"X1\",\"cmd\":\"press\",\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("button", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "button", "", + payload_press.c_str(), "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBBT && strcmp(prop.key().c_str(), "open") == 0) { + value_template = "{% if value_json.direction == \"up\" -%} {{ 100 - value_json.open/2 }}{% elif value_json.direction == \"down\" %}{{ value_json.open/2 }}{% else %} {{ value_json.open/2 }}{%- endif %}"; + String command_template = "{\"model_id\":\"W270160X\",\"tilt\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("cover", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "cover", value_template.c_str(), + "50", "", "", 0, + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + "blind", nullptr, nullptr, nullptr, command_template.c_str()); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBCU && strcmp(prop.key().c_str(), "position") == 0) { + String command_template = "{\"model_id\":\"W070160X\",\"position\":{{ value | int }},\"id\":\"" + String(p->macAdr) + "\"}"; + createDiscovery("cover", //set Type + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "cover", "{{ value_json.position }}", + "0", "100", "", 0, + Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + "curtain", nullptr, nullptr, nullptr, command_template.c_str()); + } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && + strcmp(prop.key().c_str(), "weighing_mode") == 0) { + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "enum", value_template.c_str(), + "", "", prop.value()["unit"], + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassMeasurement, nullptr, nullptr, "[\"person\",\"object\"]"); + } else if ((p->sensorModel_id == TheengsDecoder::XMTZC04HMKG || p->sensorModel_id == TheengsDecoder::XMTZC04HMLB || p->sensorModel_id == TheengsDecoder::XMTZC05HMKG || p->sensorModel_id == TheengsDecoder::XMTZC05HMLB) && + strcmp(prop.key().c_str(), "unit") == 0) { + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, "enum", value_template.c_str(), + "", "", prop.value()["unit"], + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassMeasurement, nullptr, nullptr, "[\"lb\",\"kg\",\"jin\"]"); + } else if (strcmp(prop.value()["unit"], "string") == 0 && strcmp(prop.key().c_str(), "mac") != 0) { + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "", "", "", + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (p->sensorModel_id == TheengsDecoder::MUE4094RT && strcmp(prop.value()["unit"], "status") == 0) { // This device does not a broadcast when there is nothing detected so adding a timeout + createDiscovery("binary_sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "True", "False", "", + BTConfig.presenceAwayTimer / 1000, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (strcmp(prop.value()["unit"], "status") == 0) { + createDiscovery("binary_sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "True", "False", "", + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } else if (strcmp(prop.key().c_str(), "device") != 0 && strcmp(prop.key().c_str(), "mac") != 0) { // Exception on device and mac as these ones are not sensors + createDiscovery("sensor", + discovery_topic.c_str(), entity_name.c_str(), unique_id.c_str(), + will_Topic, prop.value()["name"], value_template.c_str(), + "", "", prop.value()["unit"], + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassMeasurement); + } + } + } + } 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) { + // 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"); + } + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { + // Sensor discovery + BM2Discovery(macWOdots.c_str(), "BM2"); + // Device tracker discovery + String tracker_id = macWOdots + "-tracker"; + createDiscovery("device_tracker", + discovery_topic.c_str(), "BM2-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"); + } + if (p->sensorModel_id == BLEconectable::id::MHO_C401) { + MHO_C401Discovery(macWOdots.c_str(), "MHO-C401"); + } + if (p->sensorModel_id == BLEconectable::id::XMWSDJ04MMC) { + XMWSDJ04MMCDiscovery(macWOdots.c_str(), "XMWSDJ04MMC"); + } + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC) { + HHCCJCY01HHCCDiscovery(macWOdots.c_str(), "HHCCJCY01HHCC"); + } + } else { + Log.trace(F("Device UNKNOWN_MODEL %s" CR), p->macAdr); + } + } + } + p->isDisc = true; // we don't need the semaphore and all the search magic via createOrUpdateDevice + } else { + Log.trace(F("Device already discovered or that doesn't require discovery %s" CR), p->macAdr); + } + } +} +# else +void launchBTDiscovery(bool overrideDiscovery) {} +# endif + +# if BLEDecryptor +// ** TODO - Hex string to bytes, there is probably a function for this already just need to find it +int hexToBytes(String hex, uint8_t *out, size_t maxLen) { + int len = hex.length(); + if (len % 2 || len / 2 > maxLen) return -1; + for (int i = 0, j = 0; i < len; i += 2, j++) { + out[j] = (uint8_t) strtol(hex.substring(i, i + 2).c_str(), nullptr, 16); + } + return len / 2; +} +// Reverse bytes +void reverseBytes(uint8_t *data, size_t length) { + size_t i; + for (i = 0; i < length / 2; i++) { + uint8_t temp = data[i]; + data[i] = data[length - 1 - i]; + data[length - 1 - i] = temp; + } +} +# endif + +# if BLEDecoder +void process_bledata(JsonObject& BLEdata) { + yield(); // Necessary to let the loop run in case of connectivity issues + if (!BLEdata.containsKey("id")) { + Log.error(F("No mac address in the payload" CR)); + return; + } + const char* mac = BLEdata["id"].as(); + Log.trace(F("Processing BLE data %s" CR), BLEdata["id"].as()); + int model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); + int mac_type = BLEdata["mac_type"].as(); + +# if BLEDecryptor + if (BLEdata["encr"] && (BLEdata["encr"].as() >0 && BLEdata["encr"].as() <=2)) { + // Decrypting Encrypted BLE Data PVVX, BTHome or Victron + Log.trace(F("[BLEDecryptor] Decrypt ENCR:%d ModelID:%s Payload:%s" CR), BLEdata["encr"].as(), BLEdata["model_id"].as(), BLEdata["cipher"].as()); + + // MAC address + String macWOdots = BLEdata["id"].as(); // Mac Address without dots + macWOdots.replace(":", ""); + unsigned char macAddress[6]; + int maclen = hexToBytes(macWOdots, macAddress, 6); + if (maclen != 6) { + Log.error(F("[BLEDecryptor] Invalid MAC Address length %d" CR), maclen); + return; + } + + // AES decryption key + unsigned char bleaeskey[16]; + int bleaeskeylength = 0; + if (ble_aes_keys.containsKey(macWOdots)){ + Log.trace(F("[BLEDecryptor] Custom AES key %s" CR), ble_aes_keys[macWOdots].as()); + bleaeskeylength = hexToBytes(ble_aes_keys[macWOdots], bleaeskey, 16); + } else { + Log.trace(F("[BLEDecryptor] Default AES key" CR)); + bleaeskeylength = hexToBytes(ble_aes, bleaeskey, 16); + } + // Check AES Key + if (bleaeskeylength != 16) { + Log.error(F("[BLEDecryptor] Invalid key length %d" CR), bleaeskeylength); + return; + } + + // Build nonce and aad + uint8_t nonce[16]; + int noncelength = 0; + unsigned char aad[1]; + int aadLength; + + if (BLEdata["encr"].as() == 1){ // PVVX Encrypted + noncelength = 11; // 11 bytes + reverseBytes(macAddress, 6); // 6 bytes: device address in reverse + memcpy(nonce, macAddress, 6); + int maclen = hexToBytes(macWOdots, macAddress, 6); + + unsigned char servicedata[16]; + int servicedatalen = hexToBytes(BLEdata["servicedata"].as(), servicedata, 16); + nonce[6] = servicedatalen + 3; // 1 byte : length of (service data + type and UUID) + nonce[7] = 0x16; // 1 byte : "16" -> AD type for "Service Data - 16-bit UUID" + nonce[8] = 0x1A; // 2 bytes: "1a18" -> UUID 181a in little-endian + nonce[9] = 0x18; // + unsigned char ctr[1]; // 1 byte : counter + int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 1); + if (ctrlen != 1) { + Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); + return; + } + nonce[10] = ctr[0]; + aad[0] = 0x11; + aadLength = 1; + Log.trace(F("[BLEDecryptor] PVVX nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); + + } else if (BLEdata["encr"].as() == 2){ // BTHome V2 Encrypted + noncelength = 13; // 13 bytes + memcpy(nonce, macAddress, 6); + nonce[6] = 0xD2; // UUID + nonce[7] = 0xFC; + nonce[8] = 0x41; // BTHome Device Data encrypted payload byte + unsigned char ctr[4]; // Counter + int ctrlen = hexToBytes(BLEdata["ctr"].as(), ctr, 4); + if (ctrlen != 4) { + Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen); + return; + } + memcpy(&nonce[9], ctr, 4); + aad[0] = 0x00; + aadLength = 0; + Log.trace(F("[BLEDecryptor] BTHomeV2 nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str()); + + } else if (BLEdata["encr"].as() == 3){ + nonce[16] = {0}; // Victron has a 16 byte zero padded nonce with IV bytes 6,7 + unsigned char iv[2]; + int ivlen = hexToBytes(BLEdata["ctr"].as(), iv, 2); + if (ivlen != 2) { + Log.error(F("[BLEDecryptor] Invalid iv length %d" CR), ivlen); + return; + } + memcpy(nonce, iv, 2); + Log.trace(F("[BLEDecryptor] Victron nonce %s" CR), NimBLEUtils::dataToHexString(nonce, 16).c_str()); + } else { + return; // No match + } + + // Ciphertext to bytes + int cipherlen = sizeof(BLEdata["cipher"].as()); + unsigned char ciphertext[cipherlen]; + int ciphertextlen = hexToBytes(BLEdata["cipher"].as(), ciphertext, cipherlen); + unsigned char decrypted[ciphertextlen]; // Decrypted payload + + // Decrypt ciphertext + if (BLEdata["encr"].as() == 1 || BLEdata["encr"].as() == 2) { + // Decrypt PVVX and BTHome V2 ciphertext using AES CCM + mbedtls_ccm_context ctx; + mbedtls_ccm_init(&ctx); + if (mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, bleaeskey, 128) != 0) { + Log.error(F("[BLEDecryptor] Failed to set AES key to mbedtls" CR)); + return; + } + + // Message Integrity Check (MIC) + unsigned char mic[4]; + int miclen = hexToBytes(BLEdata["mic"].as(), mic, 4); + if (miclen != 4) { + Log.error(F("[BLEDecryptor] Invalid MIC length %d" CR), miclen); + return; + } + + int ret = mbedtls_ccm_auth_decrypt( + &ctx, // AES Key + ciphertextlen, // length of ciphertext + nonce, noncelength, // Nonce + aad, aadLength, // AAD + ciphertext, // input ciphertext + decrypted, // output plaintext + mic, sizeof(mic) // Message Integrity Check + ); + mbedtls_ccm_free(&ctx); + + if (ret == 0) { + Log.notice(F("[BLEDecryptor] Decryption successful" CR)); + } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { + Log.error(F("[BLEDecryptor] Authentication failed." CR)); + return; + } else { + Log.error(F("[BLEDecryptor] Decryption failed with error: %X" CR), ret); + return; + } + + // Build new servicedata + if (BLEdata["encr"].as() == 1){ // PVVX + BLEdata["servicedata"] = NimBLEUtils::dataToHexString(decrypted, ciphertextlen); + } else if (BLEdata["encr"].as() == 2) { // BTHomeV2 + // Build new servicedata + uint8_t newservicedata[3 + ciphertextlen]; + newservicedata[0] = 0x40; // Decrypted BTHomeV2 Packet Type + newservicedata[1] = 0x00; // Packet counter which the PVVX BTHome non-encrypted has but the encrypted does not + newservicedata[2] = 0x00; // **TODO Convert the ctr to the packet counter or just stick with 0? + memcpy(&newservicedata[3], decrypted, ciphertextlen); + BLEdata["servicedata"] = NimBLEUtils::dataToHexString(newservicedata, ciphertextlen + 3); + } else { + return; + } + Log.trace(F("[BLEDecryptor] Decrypted servicedata %s" CR), BLEdata["servicedata"].as()); + + } else if (BLEdata["encr"].as() == 3) { + // Decrypt Victron Energy encrypted advertisements. + size_t nc_off = 0; + uint8_t stream_block[16] = {0}; + + mbedtls_aes_context ctx; + mbedtls_aes_init(&ctx); + mbedtls_aes_setkey_enc(&ctx, bleaeskey, 128); + int ret = mbedtls_aes_crypt_ctr( + &ctx, // AES Key + ciphertextlen, // length of ciphertext + &nc_off, + nonce, // 16 byte nonce with 2 bytes iv + stream_block, + ciphertext, // input ciphertext + decrypted // output plaintext + ); + mbedtls_aes_free(&ctx); + + if (ret == 0) { + Log.notice(F("[BLEDecryptor] Victron Decryption successful" CR)); + } else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) { + Log.error(F("[BLEDecryptor] Victron Authentication failed." CR)); + return; + } else { + Log.error(F("[BLEDecryptor] Victron decryption failed with error: %X" CR), ret); + return; + } + + // Build new manufacturerdata + unsigned char manufacturerdata[10 + ciphertextlen]; + int manufacturerdatalen = hexToBytes(BLEdata["manufacturerdata"].as(), manufacturerdata, 10); + manufacturerdata[2] = 0x11; // Replace byte 2 with "11" indicate decrypted data + manufacturerdata[7] = 0xff; // Replace byte 7 with "ff" to indicate decrypted data + manufacturerdata[8] = 0xff; // Replace byte 8 with "ff" to indicate decrypted data + memcpy(&manufacturerdata[8], decrypted, ciphertextlen); // Append the decrypted payload to the manufacturer data + BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString(manufacturerdata, 10 + ciphertextlen); // Rebuild manufacturerdata + Log.trace(F("[BLEDecryptor] Victron decrypted manufacturerdata %s" CR), BLEdata["manufacturerdata"].as()); + } + + // Print before and after decoder post decryption + // serializeJsonPretty(BLEdata, Serial); + model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata); + // serializeJsonPretty(BLEdata, Serial); + Log.trace(F("[BLEDecryptor] Decrypted model_id %d" CR), model_id); + + // Remove the cipher fields from BLEdata + BLEdata.remove("encr"); + BLEdata.remove("cipher"); + BLEdata.remove("ctr"); + BLEdata.remove("mic"); + + } +# endif + + // Convert prmacs to RMACS until or if OMG gets Identity MAC/IRK decoding + if (BLEdata["prmac"]) { + BLEdata.remove("prmac"); + if (BLEdata["track"]) { + BLEdata.remove("track"); + } + BLEdata["type"] = "RMAC"; + Log.trace(F("Potential RMAC (prmac) converted to RMAC" CR)); + } + const char* deviceName = BLEdata["name"] | ""; + + 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 + 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 + createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); + } else { + createOrUpdateDevice(mac, device_flags_init, model_id, mac_type, deviceName); + if (BTConfig.adaptiveScan == true && (BTConfig.BLEinterval != MinTimeBtwScan || BTConfig.intervalActiveScan != MinTimeBtwScan)) { + if (BLEdata.containsKey("acts") && BLEdata.containsKey("cont")) { + if (BLEdata["acts"] && BLEdata["cont"]) { + BTConfig.BLEinterval = MinTimeBtwScan; + BTConfig.intervalActiveScan = MinTimeBtwScan; + BTConfig.scanDuration = MinScanDuration; + Log.notice(F("Active and continuous scanning required, parameters adapted" CR)); + stateBTMeasures(false); + } + } else if (BLEdata.containsKey("cont") && BTConfig.BLEinterval != MinTimeBtwScan) { + if (BLEdata["cont"]) { + BTConfig.BLEinterval = MinTimeBtwScan; + if ((BLEdata["type"].as()).compare("CTMO") == 0) { + BTConfig.scanDuration = MinScanDuration; + } + Log.notice(F("Passive continuous scanning required, parameters adapted" CR)); + stateBTMeasures(false); + } + } + } + } + } else { + if (BLEdata.containsKey("name")) { // Connectable only devices + std::string name = BLEdata["name"]; + if (name.compare("LYWSD03MMC") == 0) + model_id = BLEconectable::id::LYWSD03MMC; + else if (name.compare("DT24-BLE") == 0) + model_id = BLEconectable::id::DT24_BLE; + else if (name.compare("MHO-C401") == 0) + model_id = BLEconectable::id::MHO_C401; + else if (name.compare("XMWSDJ04MMC") == 0) + model_id = BLEconectable::id::XMWSDJ04MMC; + + if (model_id > 0) { + Log.trace(F("Connectable device found: %s" CR), name.c_str()); + createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); + } + } else if (BTConfig.extDecoderEnable && model_id < 0 && BLEdata.containsKey("servicedata")) { + const char* service_data = (const char*)(BLEdata["servicedata"] | ""); + if (strstr(service_data, "209800") != NULL) { + model_id = TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC; + Log.trace(F("Connectable device found: HHCCJCY01HHCC" CR)); + createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); + } + } + } + } else { + Log.trace(F("Random MAC or iBeacon device filtered" CR)); + } + if (!BTConfig.extDecoderEnable && model_id < 0) { + Log.trace(F("No eligible device found " CR)); + } +} +void PublishDeviceData(JsonObject& BLEdata) { + if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough + // Decode the payload + process_bledata(BLEdata); + // If the device is a random MAC and pubRandomMACs is false we don't publish this payload + if (!BTConfig.pubRandomMACs && (BLEdata["type"].as()).compare("RMAC") == 0) { + Log.trace(F("Random MAC, device filtered" CR)); + return; + } + // If pubAdvData is false we don't publish the adv data + if (!BTConfig.pubAdvData) { + BLEdata.remove("servicedatauuid"); + BLEdata.remove("servicedata"); + BLEdata.remove("manufacturerdata"); + BLEdata.remove("mac_type"); + BLEdata.remove("adv_type"); + // tag device properties + // BLEdata.remove("type"); type is used by the WebUI module to determine the template used to display the signal + BLEdata.remove("cidc"); + BLEdata.remove("acts"); + BLEdata.remove("cont"); + BLEdata.remove("track"); + BLEdata.remove("ctrl"); + } + // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid + if (BLEdata.containsKey("distance")) { + if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { + BLEdata["mac"] = BLEdata["id"].as(); + BLEdata["id"] = BLEdata["uuid"].as(); + } + String topic = String(mqtt_topic) + BTConfig.presenceTopic + String(gateway_name); + Log.trace(F("Pub HA Presence %s" CR), topic.c_str()); + BLEdata["topic"] = topic; + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } + + // If the device is not a sensor and pubOnlySensors is true we don't publish this payload + if (!BTConfig.pubOnlySensors || BLEdata.containsKey("model") || !BLEDecoder) { // Identified device + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } else { + Log.notice(F("Not a sensor device filtered" CR)); + return; + } + +# if BLEDecoder + if (enableMultiGTWSync && BLEdata.containsKey("model_id") && BLEdata.containsKey("id")) { + // Publish tracker sync message + bool isTracker = false; + std::string tag = decoder.getTheengAttribute(BLEdata["model_id"].as(), "tag"); + if (tag.length() >= 4) { + isTracker = checkIfIsTracker(tag[3]); + } + + if (isTracker) { + StaticJsonDocument BLEdataBuffer; + JsonObject TrackerSyncdata = BLEdataBuffer.to(); + TrackerSyncdata["gatewayid"] = gateway_name; + TrackerSyncdata["trackerid"] = BLEdata["id"].as(); + String topic = String(mqtt_topic) + String(subjectTrackerSync); + TrackerSyncdata["topic"] = topic.c_str(); + enqueueJsonObject(TrackerSyncdata); + } + } +# endif + } else { + Log.notice(F("Low rssi, device filtered" CR)); + return; + } +} +# else +void process_bledata(JsonObject& BLEdata) {} +void PublishDeviceData(JsonObject& BLEdata) { + if (abs((int)BLEdata["rssi"] | 0) < abs(BTConfig.minRssi)) { // process only the devices close enough + // if distance available, check if presenceUseBeaconUuid is true, model_id is IBEACON then set id as uuid + if (BLEdata.containsKey("distance")) { + if (BTConfig.presenceUseBeaconUuid && BLEdata.containsKey("model_id") && BLEdata["model_id"].as() == "IBEACON") { + BLEdata["mac"] = BLEdata["id"].as(); + BLEdata["id"] = BLEdata["uuid"].as(); + } + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } else { + Log.notice(F("Low rssi, device filtered" CR)); + return; + } +} +# endif + +void hass_presence(JsonObject& HomePresence) { + int BLErssi = HomePresence["rssi"]; + Log.trace(F("BLErssi %d" CR), BLErssi); + int txPower = HomePresence["txpower"] | 0; + if (txPower >= 0) + txPower = -59; //if tx power is not found we set a default calibration value + Log.trace(F("TxPower: %d" CR), txPower); + double ratio = BLErssi * 1.0 / txPower; + double distance; + if (ratio < 1.0) { + distance = pow(ratio, 10); + } else { + distance = (0.89976) * pow(ratio, 7.7095) + 0.111; + } + HomePresence["distance"] = distance; + Log.trace(F("Ble distance %D" CR), distance); +} + +void BTforceScan() { + if (!BTProcessLock) { + BLEscan(); + Log.trace(F("Scan done" CR)); + if (BTConfig.bleConnect) + BLEconnect(); + } else { + Log.trace(F("Cannot launch scan due to other process running" CR)); + } +} + +void immediateBTAction(void* pvParameters) { + if (BLEactions.size()) { + // Immediate action; we need to prevent the normal connection action and stop scanning + BTProcessLock = true; + NimBLEScan* pScan = NimBLEDevice::getScan(); + if (pScan->isScanning()) { + pScan->stop(); + } + + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { + // swap the vectors so only this device is processed + std::vector dev_swap; + dev_swap.push_back(getDeviceByMac(BLEactions.back().addr.toString().c_str())); + std::swap(devices, dev_swap); + + std::vector act_swap; + act_swap.push_back(BLEactions.back()); + BLEactions.pop_back(); + std::swap(BLEactions, act_swap); + + // Unlock here to allow the action to be performed + BTProcessLock = false; + BLEconnect(); + // back to normal + std::swap(devices, dev_swap); + std::swap(BLEactions, act_swap); + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + } else { + Log.error(F("CreateOrUpdate Semaphore NOT taken" CR)); + } + + // If we stopped the scheduled connect for this action, do the scheduled now + if (millis() > (timeBetweenConnect + BTConfig.intervalConnect) && BTConfig.bleConnect) { + timeBetweenConnect = millis(); + BLEconnect(); + } + xSemaphoreGive(semaphoreBLEOperation); + } else { + Log.error(F("BLE busy - immediateBTAction not sent" CR)); + gatewayState = GatewayState::ERROR; + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = BLEactions.back().addr.toString(); + BLEdata["success"] = false; + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + BLEactions.pop_back(); + BTProcessLock = false; + } + } + vTaskDelete(NULL); +} + +void startBTActionTask() { + TaskHandle_t th; + xTaskCreateUniversal( + immediateBTAction, /* Function to implement the task */ + "imActTask", /* Name of the task */ + 8000, /* Stack size in bytes */ + NULL, /* Task input parameter */ + 3, /* Priority of the task (set higher than core task) */ + &th, /* Task handle. */ + 1); /* Core where the task should run */ +} + +# if BLEDecoder +void KnownBTActions(JsonObject& BTdata) { + if (!BTdata.containsKey("id")) { + Log.error(F("BLE mac address missing" CR)); + gatewayState = GatewayState::ERROR; + return; + } + + BLEAction action{}; + action.write = true; + action.ttl = 3; + bool res = false; + if (BTdata.containsKey("model_id") && BTdata["model_id"].is()) { + if (BTdata["model_id"] == "X1") { + if (BTdata.containsKey("cmd") && BTdata["cmd"].is()) { + action.value_type = BLE_VAL_STRING; + std::string val = BTdata["cmd"].as(); // Fix #1694 + action.value = val; + createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBS1, 1); + res = true; + } + } else if (BTdata["model_id"] == "W270160X") { + if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { + action.value_type = BLE_VAL_INT; + res = true; + } else if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { + action.value_type = BLE_VAL_STRING; + res = true; + } + if (res) { + std::string val = BTdata["tilt"].as(); // Fix #1694 + action.value = val; + createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBBT, 1); + } + } else if (BTdata["model_id"] == "W070160X") { + if (BTdata.containsKey("position") && BTdata["position"].is()) { + action.value_type = BLE_VAL_INT; + res = true; + } else if (BTdata.containsKey("position") && BTdata["position"].is()) { + action.value_type = BLE_VAL_STRING; + res = true; + } + if (res) { + std::string val = BTdata["position"].as(); // Fix #1694 + action.value = val; + createOrUpdateDevice(BTdata["id"].as(), device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBCU, 1); + } + } + if (res) { + action.addr = NimBLEAddress(BTdata["id"].as(), 1); + BLEactions.push_back(action); + startBTActionTask(); + } else { + Log.error(F("BLE action not recognized" CR)); + gatewayState = GatewayState::ERROR; + } + } +} +# else +void KnownBTActions(JsonObject& BTdata) {} +# endif + +void XtoBTAction(JsonObject& BTdata) { + BLEAction action{}; + action.ttl = BTdata.containsKey("ttl") ? (uint8_t)BTdata["ttl"] : 1; + action.value_type = BLE_VAL_STRING; + if (BTdata.containsKey("value_type")) { + String vt = BTdata["value_type"]; + vt.toUpperCase(); + if (vt == "HEX") + action.value_type = BLE_VAL_HEX; + else if (vt == "INT") + action.value_type = BLE_VAL_INT; + else if (vt == "FLOAT") + action.value_type = BLE_VAL_FLOAT; + else if (vt != "STRING") { + Log.error(F("BLE value type invalid %s" CR), vt.c_str()); + return; + } + } + + Log.trace(F("BLE ACTION TTL = %u" CR), action.ttl); + action.complete = false; + if (BTdata.containsKey("ble_write_address") && + BTdata.containsKey("ble_write_service") && + BTdata.containsKey("ble_write_char") && + BTdata.containsKey("ble_write_value")) { + action.addr = NimBLEAddress(BTdata["ble_write_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); + action.service = NimBLEUUID((const char*)BTdata["ble_write_service"]); + action.characteristic = NimBLEUUID((const char*)BTdata["ble_write_char"]); + std::string val = BTdata["ble_write_value"].as(); // Fix #1694 + action.value = val; + action.write = true; + Log.trace(F("BLE ACTION Write" CR)); + } else if (BTdata.containsKey("ble_read_address") && + BTdata.containsKey("ble_read_service") && + BTdata.containsKey("ble_read_char")) { + action.addr = NimBLEAddress(BTdata["ble_read_address"].as(), BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0); + action.service = NimBLEUUID((const char*)BTdata["ble_read_service"]); + action.characteristic = NimBLEUUID((const char*)BTdata["ble_read_char"]); + action.write = false; + Log.trace(F("BLE ACTION Read" CR)); + } else { + return; + } + + createOrUpdateDevice(action.addr.toString().c_str(), device_flags_connect, UNKWNON_MODEL, action.addr.getType()); + + BLEactions.push_back(action); + if (BTdata.containsKey("immediate") && BTdata["immediate"].as()) { + startBTActionTask(); + } +} + +void XtoBT(const char* topicOri, JsonObject& BTdata) { // json object decoding + if (cmpToMainTopic(topicOri, subjectMQTTtoBTset)) { + Log.trace(F("MQTTtoBT json set" CR)); + + // Black list & white list set + bool WorBupdated; + WorBupdated = updateWorB(BTdata, true); + WorBupdated |= updateWorB(BTdata, false); + + if (WorBupdated) { + if (xSemaphoreTake(semaphoreCreateOrUpdateDevice, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdTRUE) { + //dumpDevices(); + xSemaphoreGive(semaphoreCreateOrUpdateDevice); + } + } + + // Force scan now + if (BTdata.containsKey("interval") && BTdata["interval"] == 0) { + Log.notice(F("BLE forced scan" CR)); + atomic_store_explicit(&forceBTScan, 1, ::memory_order_seq_cst); // ask the other core to do the scan for us + } + + /* + * Configuration modifications priorities: + * First `init=true` and `load=true` commands are executed (if both are present, INIT prevails on LOAD) + * Then parameters included in json are taken in account + * Finally `erase=true` and `save=true` commands are executed (if both are present, ERASE prevails on SAVE) + */ + if (BTdata.containsKey("init") && BTdata["init"].as()) { + // Restore the default (initial) configuration + BTConfig_init(); + } else if (BTdata.containsKey("load") && BTdata["load"].as()) { + // Load the saved configuration, if not initialised + BTConfig_load(); + } + + // Load config from json if available + BTConfig_fromJson(BTdata); + + } else if (cmpToMainTopic(topicOri, subjectMQTTtoBT)) { + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { + KnownBTActions(BTdata); + XtoBTAction(BTdata); + xSemaphoreGive(semaphoreBLEOperation); + } else { + Log.error(F("BLE busy - BTActions not sent" CR)); + gatewayState = GatewayState::ERROR; + } + } else if (strstr(topicOri, subjectTrackerSync) != NULL) { + if (BTdata.containsKey("gatewayid") && BTdata.containsKey("trackerid") && BTdata["gatewayid"] != gateway_name) { + BLEdevice* device = getDeviceByMac(BTdata["trackerid"].as()); + if (device != &NO_BT_DEVICE_FOUND && device->lastUpdate != 0) { + device->lastUpdate = 0; + Log.notice(F("Tracker %s disassociated by gateway %s" CR), BTdata["trackerid"].as(), BTdata["gatewayid"].as()); + } + } + } +} +#endif diff --git a/main/mqttDiscovery.cpp b/main/mqttDiscovery.cpp index 89da508bac..9f9b008a6f 100644 --- a/main/mqttDiscovery.cpp +++ b/main/mqttDiscovery.cpp @@ -1,1568 +1,1563 @@ -/* - OpenMQTTGateway Addon - ESP8266 or Arduino program for home automation - - Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker - Send and receiving command by MQTT - - This is the Home Assistant MQTT Discovery addon. - - Copyright: (c) Rafal Herok - - This file is part of OpenMQTTGateway. - - OpenMQTTGateway is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - OpenMQTTGateway is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#include "User_config.h" - -#ifdef ZmqttDiscovery -# include "TheengsCommon.h" - -# ifdef ESP8266 -# include -# elif defined(ESP32) -# include - -# include "esp_mac.h" -# endif -# ifdef ESP32_ETHERNET -# include -# endif -# include "config_mqttDiscovery.h" - -extern bool ethConnected; -extern JsonArray modules; - -char discovery_prefix[parameters_size + 1] = discovery_Prefix; -// From https://github.com/home-assistant/core/blob/d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf/homeassistant/const.py#L225 -// List of classes available in Home Assistant -const char* availableHASSClasses[] = {"battery_charging", - "battery", - "carbon_dioxide", - "carbon_monoxide", - "connectivity", - "current", - "data_size", - "distance", - "door", - "duration", - "energy", - "enum", - "frequency", - "gas", - "humidity", - "illuminance", - "irradiance", - "lock", - "motion", - "moving", - "occupancy", - "pm1", - "pm10", - "pm25", - "power_factor", - "power", - "precipitation_intensity", - "precipitation", - "pressure", - "problem", - "restart", - "signal_strength", - "sound_pressure", - "temperature", - "timestamp", - "voltage", - "water", - "weight", - "wind_speed", - "window"}; - -// From https://github.com/home-assistant/core/blob/d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf/homeassistant/const.py#L379 -// List of units available in Home Assistant -const char* availableHASSUnits[] = {"A", - "B", - "UV index", - "V", - "W", - "W", - "bpm", - "bar", - "cm", - "dB", - "dBm", - "ft", - "h", - "hPa", - "Hz", - "kg", - "kW", - "kWh", - "km/h", - "lb", - "lx", - "m/s", - "m/s²", - "m³", - "mg/m³", - "min", - "mm", - "mm/h", - "ms", - "mV", - "µS/cm", - "μg/m³", - "Ω", - "%", - "°", - "°C", - "°F", - "s", - "wb²" -}; - -String getMacAddress() { - uint8_t baseMac[6]; - char baseMacChr[13] = {0}; -# if defined(ESP8266) - WiFi.macAddress(baseMac); - sprintf(baseMacChr, "%02X%02X%02X%02X%02X%02X", baseMac[0], baseMac[1], baseMac[2], baseMac[3], baseMac[4], baseMac[5]); -# elif defined(ESP32) - esp_read_mac(baseMac, ESP_MAC_WIFI_STA); - sprintf(baseMacChr, "%02X%02X%02X%02X%02X%02X", baseMac[0], baseMac[1], baseMac[2], baseMac[3], baseMac[4], baseMac[5]); -# else - sprintf(baseMacChr, "%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); -# endif - return String(baseMacChr); -} - -String getUniqueId(String name, String sufix) { - String uniqueId = (String)getMacAddress() + "-" + name + sufix; - return String(uniqueId); -} - -# if defined(ZgatewayBT) || defined(SecondaryModule) -# include "config_BT.h" -/** - * Create a discover messages form a list of attribute - * - * @param mac the MAC address - * @param sensorList[][0] = component type - * @param sensorList[][1] = name - * @param sensorList[][2] = availability topic - * @param sensorList[][3] = device class - * @param sensorList[][4] = value template - * @param sensorList[][5] = payload on - * @param sensorList[][6] = payload off - * @param sensorList[][7] = unit of measurement - * @param sensorList[][8] = unit of measurement - * @param sensorCount number of sensor - * @param device_name name of sensors - * @param device_manufacturer name of manufacturer - * @param device_model the model - * */ -void createDiscoveryFromList(const char* mac, - const char* sensorList[][9], - int sensorCount, - const char* device_name, - const char* device_manufacturer, - const char* device_model) { - for (int i = 0; i < sensorCount; i++) { - String discovery_topic = String(subjectBTtoMQTT) + "/" + String(mac); - String unique_id = String(mac) + "-" + sensorList[i][1]; - - createDiscovery(sensorList[i][0], - discovery_topic.c_str(), sensorList[i][1], unique_id.c_str(), - will_Topic, sensorList[i][3], sensorList[i][4], - sensorList[i][5], sensorList[i][6], sensorList[i][7], - 0, "", "", false, "", - device_name, device_manufacturer, device_model, mac, false, - sensorList[i][8] //The state class - ); - } -} -# endif - -/** - * @brief Announce that the Gateway have the ability to raise Trigger. - * This function provide the configuration of the MQTT Device trigger ( @see https://www.home-assistant.io/integrations/device_trigger.mqtt/ ). - * All messages published by this function will be interpreted as configuration messages of Gateway Triggers. - * Instead, all messages published on the "triggerTopic" will be interpreted as Gateway trigger. - * - * @param triggerTopic Mandatory - The MQTT topic subscribed to receive trigger events. - * @param type The type of the trigger, e.g. button_short_press. Entries supported by the HA Frontend: button_short_press, button_short_release, button_long_press, button_long_release, button_double_press, button_triple_press, button_quadruple_press, button_quintuple_press. If set to an unsupported value, will render as subtype type, e.g. button_1 spammed with type set to spammed and subtype set to button_1 - * @param subtype The subtype of the trigger, e.g. button_1. Entries supported by the HA frontend: turn_on, turn_off, button_1, button_2, button_3, button_4, button_5, button_6. If set to an unsupported value, will render as subtype type, e.g. left_button pressed with type set to button_short_press and subtype set to left_button - * @param object_id The object_id of the trigger. - * @param value_template The template to render the value of the trigger. The template can use the variables trigger.id, trigger.type, trigger.subtype, trigger.payload, trigger.payload_json, trigger.topic, trigger.timestamp, trigger.value, trigger.value_json. The template can be a string or a JSON object. If the template is a JSON object, it must be a valid JSON object. If the template is a string, it will be rendered as a string. If the template is a JSON object, it will be rendered as a JSON object. - */ -void announceGatewayTrigger(const char* triggerTopic, - const char* type, - const char* subtype, - const char* object_id, - const char* value_template) { - //Create The Json - StaticJsonDocument jsonBuffer; - JsonObject sensor = jsonBuffer.to(); - - /** - * The type of automation, must be ‘trigger’. - * @see https://www.home-assistant.io/integrations/device_trigger.mqtt/#automation_type - */ - sensor["automation_type"] = "trigger"; - - /** - * Must be device_automation. Only allowed and required in MQTT auto discovery device messages. - * @see https://www.home-assistant.io/integrations/device_trigger.mqtt/#platform - * @see https://www.home-assistant.io/integrations/mqtt/#device-discovery-payload - */ - sensor["platform "] = "device_automation"; - - // The MQTT topic subscribed to receive trigger events. - if (triggerTopic && triggerTopic[0]) { - char state_topic[mqtt_topic_max_size]; - - strcpy(state_topic, mqtt_topic); - strcat(state_topic, gateway_name); - strcat(state_topic, triggerTopic); - - /** - * "info_topic" is not a standard field, for the message is required the filed "topic", but this filed is reserved and it is used to know where to publish the topic. - * If we want to send on the message the topic information is usefull to use this "info_topic" that will be not delete by the send function but converted to "topic" - */ - sensor["info_topic"] = state_topic; - } else { - Log.error(F("[RF] Error: topic is mandatory for device trigger Discovery" CR)); - return; - } - - /** - * The type of the trigger, e.g. button_short_press. - * Entries supported by the HA frontend: button_short_press, button_short_release, button_long_press, button_long_release, button_double_press, button_triple_press, button_quadruple_press, button_quintuple_press. - * If set to an unsupported value, will render as subtype type, e.g. button_1 spammed with type set to spammed and subtype set to button_1 - */ - if (type && type[0] != 0) { - sensor["type"] = type; - } else { - sensor["type"] = "button_short_press"; - } - - /** - * The subtype of the trigger, e.g. turn_on. - * Entries supported by the frontend: turn_on, turn_off, button_1, button_2, button_3, button_4, button_5, button_6. - * If set to an unsupported value, will render as subtype type, e.g. left_button pressed with type set to button_short_press and subtype set to left_button - */ - if (subtype && subtype[0] != 0) { - sensor["subtype"] = subtype; - } else { - sensor["subtype"] = "turn_on"; - } - - // ------------------ START DEVICE DECLARATION -------------------------------------------------- - // TODO: This section, like the almost identical one in createDiscovery, should be placed in a - // separate function and managed specifically to avoid errors in representing the device - // in the HASS world. - // ------------------------------------------------------------------------------------------------- - - // Information about the device: this device trigger is a part of to tie it into the HA device registry. - StaticJsonDocument jsonDeviceBuffer; - JsonObject device = jsonDeviceBuffer.to(); - - // A link to the webpage that can manage the configuration of this device. - if (ethConnected) { -# ifdef ESP32_ETHERNET - device["configuration_url"] = String("http://") + String(ETH.localIP().toString()) + String("/"); //configuration_url -# endif - } else { - device["configuration_url"] = String("http://") + String(WiFi.localIP().toString()) + String("/"); //configuration_url - } - - /* - * A list of connections of the device to the outside world as a list of tuples [connection_type, connection_identifier]. - * For example the MAC address of a network interface: "connections": [["mac", "02:5b:26:a8:dc:12"]]. - */ - JsonArray connections = device.createNestedArray("connections"); - JsonArray connection_mac = connections.createNestedArray(); - connection_mac.add("mac"); - connection_mac.add(getMacAddress()); - - // A list of IDs that uniquely identify the device. For example a serial number. - String unique_id = String(getMacAddress()); - JsonArray identifiers = device.createNestedArray("identifiers"); - identifiers.add(unique_id); - - // The manufacturer of the device. - device["mf"] = GATEWAY_MANUFACTURER; - - // The model of the device. -# ifndef GATEWAY_MODEL - String model = ""; - serializeJson(modules, model); - device["mdl"] = model; -# else - device["mdl"] = GATEWAY_MODEL; -# endif - - // The name of the device. - device["name"] = String(gateway_name); - device["sw"] = OMG_VERSION; - // ------------------ END DEVICE DECLARATION ------------------ // - - sensor["device"] = device; //device representing the board - - if (value_template && value_template[0]) { - sensor["value_template"] = String(value_template); - } - - /* Publish on the topic - The discovery topic needs to be: /device_automation/[/]/config. - - Note that only one trigger may be defined per unique discovery topic. - Also note that the combination of type and subtype should be unique for a device. - - */ - - String topic_to_publish = String(discovery_prefix) + "/device_automation/" + String(unique_id) + "/" + object_id + "/config"; - Log.trace(F("Announce Gatewy Trigger %s" CR), topic_to_publish.c_str()); - sensor["topic"] = topic_to_publish; - sensor["retain"] = true; - enqueueJsonObject(sensor); -} - -/* - * Remove a substring p from a given string s -*/ -std::string remove_substring(std::string s, const std::string& p) { - std::string::size_type n = p.length(); - - for (std::string::size_type i = s.find(p); - i != std::string::npos; - i = s.find(p)) - s.erase(i, n); - - return s; -} - -/** - * @brief Generate message and publish it on an MQTT discovery explorer. For HA @see https://www.home-assistant.io/docs/mqtt/discovery/ - * - * @param sensor_type the Type - * @param st_topic set state topic, - * @param s_name set name, - * @param unique_id set uniqueId - * @param availability_topic set availability_topic, - * @param device_class set device_class, - * @param value_template set value_template, - * @param payload_on set payload_on, - * @param payload_off set payload_off, - * @param unit_of_meas set unit_of_meas, - * @param off_delay set off_delay - * @param payload_available set payload_available, - * @param payload_not_available set payload_not_available - * @param gateway_entity set is a gateway entity, - * @param cmd_topic set command topic - * @param device_name set device name, - * @param device_manufacturer set device manufacturer, - * @param device_model set device model, - * @param device_id set device(BLE)/entity(RTL_433) identification, - * @param retainCmd set retain - * @param state_class set state class - * - * */ -void createDiscovery(const char* sensor_type, - const char* st_topic, const char* s_name, const char* unique_id, - const char* availability_topic, const char* device_class, const char* value_template, - const char* payload_on, const char* payload_off, const char* unit_of_meas, - int off_delay, - const char* payload_available, const char* payload_not_available, bool gateway_entity, const char* cmd_topic, - const char* device_name, const char* device_manufacturer, const char* device_model, const char* device_id, bool retainCmd, - const char* state_class, const char* state_off, const char* state_on, const char* enum_options, const char* command_template) { - StaticJsonDocument jsonBuffer; - JsonObject sensor = jsonBuffer.to(); - - // If a component cannot render it's state (f.i. KAKU relays) no state topic - // should be added. Without a state topic HA will use optimistic mode for the - // component by default. The Home Assistant UI for optimistic switches - // (separate on and off icons) allows for multiple - // subsequent on commands. This is required for dimming on KAKU relays like - // the ACM-300. - if (st_topic && st_topic[0]) { - char state_topic[mqtt_topic_max_size]; - // If not an entity belonging to the gateway we put wild card for the location and gateway name - // allowing to have the entity detected by several gateways and a consistent discovery topic among the gateways - if (gateway_entity) { - strcpy(state_topic, mqtt_topic); - strcat(state_topic, gateway_name); - } else { - strcpy(state_topic, "+/+"); - } - strcat(state_topic, st_topic); - if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { - sensor["tilt_status_t"] = state_topic; // tilt_status_topic for blind - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { - sensor["pos_t"] = state_topic; // position_topic for curtain - } else { - sensor["stat_t"] = state_topic; - } - } - - if (availability_topic && availability_topic[0] && gateway_entity) { - char avty_topic[mqtt_topic_max_size]; - strcpy(avty_topic, mqtt_topic); - strcat(avty_topic, gateway_name); - strcat(avty_topic, availability_topic); - sensor["avty_t"] = avty_topic; - } - - if (device_class && device_class[0]) { - // We check if the class belongs to HAAS classes list - int num_classes = sizeof(availableHASSClasses) / sizeof(availableHASSClasses[0]); - for (int i = 0; i < num_classes; i++) { // see class list and size into config_mqttDiscovery.h - if (strcmp(availableHASSClasses[i], device_class) == 0) { - sensor["dev_cla"] = device_class; //device_class - } - } - } - - if (unit_of_meas && unit_of_meas[0]) { - // We check if the class belongs to HAAS units list - int num_units = sizeof(availableHASSUnits) / sizeof(availableHASSUnits[0]); - for (int i = 0; i < num_units; i++) { // see units list and size into config_mqttDiscovery.h - if (strcmp(availableHASSUnits[i], unit_of_meas) == 0) { - sensor["unit_of_meas"] = unit_of_meas; //unit_of_measurement*/ - } - } - } - sensor["name"] = s_name; //name - sensor["uniq_id"] = unique_id; //unique_id - sensor["obj_id"] = unique_id; // object_id aka entity_id default value as unique_id - if (retainCmd) - sensor["retain"] = retainCmd; // Retain command - if (value_template && value_template[0]) { - if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { - sensor["tilt_status_tpl"] = value_template; // tilt_status_template for blind - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { - sensor["pos_tpl"] = value_template; // position_template for curtain - } else { - sensor["val_tpl"] = value_template; //HA Auto discovery - } - } - if (payload_on && payload_on[0]) { - if (strcmp(sensor_type, "button") == 0) { - sensor["pl_prs"] = payload_on; // payload_press for a button press - } else if (strcmp(sensor_type, "number") == 0) { - sensor["cmd_tpl"] = payload_on; // payload_on for a switch - } else if (strcmp(sensor_type, "update") == 0) { - sensor["pl_inst"] = payload_on; // payload_install for update - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { - int value = std::stoi(payload_on); - sensor["tilt_opnd_val"] = value; // tilt_open_value for blind - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { - int value = std::stoi(payload_on); - sensor["pos_open"] = value; // open value for curtain - } else { - if (strcmp(payload_on, "True") == 0 || strcmp(payload_on, "true") == 0) { - sensor["pl_on"] = true; - } else { - sensor["pl_on"] = payload_on; // payload_on for the rest - } - } - } - if (payload_off && payload_off[0]) { - if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { - sensor["pl_cls"] = payload_off; // payload_close for cover - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { - int value = std::stoi(payload_off); - sensor["pos_clsd"] = value; // closed value for curtain - } else { - if (strcmp(payload_off, "False") == 0 || strcmp(payload_off, "false") == 0) { - sensor["pl_off"] = false; - } else { - sensor["pl_off"] = payload_off; //payload_off for the rest - } - } - } - if (command_template && command_template[0]) { - if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { - sensor["tilt_cmd_tpl"] = command_template; //command_template - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { - sensor["set_pos_tpl"] = command_template; //command_template - } else { - sensor["cmd_tpl"] = command_template; //command_template - } - } - if (strcmp(sensor_type, "device_tracker") == 0) - sensor["source_type"] = "bluetooth_le"; // payload_install for update - if (off_delay != 0) - sensor["off_delay"] = off_delay; //off_delay - if (payload_available[0]) - sensor["pl_avail"] = payload_available; // payload_on - if (payload_not_available[0]) - sensor["pl_not_avail"] = payload_not_available; //payload_off - if (state_class && state_class[0]) - sensor["stat_cla"] = state_class; //add the state class on the sensors ( https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes ) - if (state_on != nullptr) - if (strcmp(state_on, "true") == 0) { - sensor["stat_on"] = true; - } else { - sensor["stat_on"] = state_on; - } - if (state_off != nullptr) - if (strcmp(state_off, "false") == 0) { - sensor["stat_off"] = false; - } else { - sensor["stat_off"] = state_off; - } - if (cmd_topic[0]) { - char command_topic[mqtt_topic_max_size]; - strcpy(command_topic, mqtt_topic); - strcat(command_topic, gateway_name); - strcat(command_topic, cmd_topic); - if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { - sensor["tilt_cmd_t"] = command_topic; // tilt_command_topic for cover - } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { - sensor["set_pos_t"] = command_topic; // position_command_topic for curtain - } else { - sensor["cmd_t"] = command_topic; //command_topic - } - } - - if (enum_options != nullptr) { - sensor["options"] = enum_options; - } - - StaticJsonDocument jsonDeviceBuffer; - JsonObject device = jsonDeviceBuffer.to(); - JsonArray identifiers = device.createNestedArray("ids"); - - if (gateway_entity) { - //device representing the board - device["name"] = String(gateway_name); -# ifndef GATEWAY_MODEL - String model = ""; - serializeJson(modules, model); - device["mdl"] = model; -# else - device["mdl"] = GATEWAY_MODEL; -# endif - device["mf"] = GATEWAY_MANUFACTURER; - if (ethConnected) { -# ifdef ESP32_ETHERNET - device["cu"] = String("http://") + String(ETH.localIP().toString()) + String("/"); //configuration_url -# endif - } else { - device["cu"] = String("http://") + String(WiFi.localIP().toString()) + String("/"); //configuration_url - } - - device["sw"] = OMG_VERSION; - identifiers.add(String(getMacAddress())); - } else { - //The Connections - if (device_id[0]) { - JsonArray connections = device.createNestedArray("cns"); - JsonArray connection_mac = connections.createNestedArray(); - connection_mac.add("mac"); - connection_mac.add(device_id); - //Device representing the actual sensor/switch device - //The Device ID - identifiers.add(device_id); - } - - if (device_manufacturer[0]) { - device["mf"] = device_manufacturer; - } - - if (device_model[0]) { - device["mdl"] = device_model; - } - - // generate unique device name by adding the second half of the device_id only if device_name and device_id are different and we don't want to use the BLE name - if (device_name[0]) { - #if defined(ZgatewayBT) // displayDeviceName only applies when running Bluetooth - if (strcmp(device_id, device_name) != 0 && device_id[0] && !displayDeviceName) { - #else !ForceDeviceName // Support ForceDeviceName for esp8266's - if (strcmp(device_id, device_name) != 0 && device_id[0] && !ForceDeviceName) { - #endif - device["name"] = device_name + String("-") + String(device_id + 6); - } else { - device["name"] = device_name; - } - } - - device["via_device"] = String(getMacAddress()); //mac address of the gateway so that the devices link to the gateway - } - - sensor["device"] = device; - - String topic = String(discovery_prefix) + "/" + String(sensor_type) + "/" + String(unique_id) + "/config"; - Log.trace(F("Announce Device %s on %s" CR), String(sensor_type).c_str(), topic.c_str()); - sensor["topic"] = topic; - sensor["retain"] = true; - enqueueJsonObject(sensor); -} - -void eraseTopic(const char* sensor_type, const char* unique_id) { - if (sensor_type == NULL || unique_id == NULL) { - return; - } - String topic = String(discovery_prefix) + "/" + String(sensor_type) + "/" + String(unique_id) + "/config"; - Log.trace(F("Erase entity discovery %s on %s" CR), String(sensor_type).c_str(), topic.c_str()); - pubMQTT((char*)topic.c_str(), "", true); -} - -# if defined(ZgatewayBT) || defined(SecondaryModule) -void btPresenceParametersDiscovery() { - createDiscovery("number", //set Type - subjectBTtoMQTT, "BT: Presence/Tracker timeout", (char*)getUniqueId("presenceawaytimer", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.presenceawaytimer/60000 }}", //set availability_topic,device_class,value_template, - "{\"presenceawaytimer\":{{value*60000}},\"save\":true}", "", "min", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, - stateClassNone //State Class - ); -} -void btScanParametersDiscovery() { - createDiscovery("number", //set Type - subjectBTtoMQTT, "BT: Interval between scans", (char*)getUniqueId("interval", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.interval/1000 }}", //set availability_topic,device_class,value_template, - "{\"interval\":{{value*1000}},\"save\":true}", "", "s", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, - stateClassNone //State Class - ); - createDiscovery("number", //set Type - subjectBTtoMQTT, "BT: Interval between active scans", (char*)getUniqueId("intervalacts", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.intervalacts/1000 }}", //set availability_topic,device_class,value_template, - "{\"intervalacts\":{{value*1000}},\"save\":true}", "", "s", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, - stateClassNone //State Class - ); -} -# endif - -void pubMqttDiscovery() { - Log.trace(F("omgStatusDiscovery" CR)); -# ifdef SecondaryModule - String uptimeName = "SYS: Uptime " + String(SecondaryModule); - String uptimeId = "uptime-" + String(SecondaryModule); - createDiscovery("sensor", //set Type - subjectSYStoMQTTSecondaryModule, uptimeName.c_str(), (char*)getUniqueId(uptimeId, "").c_str(), //set state_topic,name,uniqueId - will_Topic, "duration", "{{ value_json.uptime }}", //set availability_topic,device_class,value_template, - "", "", "s", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - String freememName = "SYS: Free memory " + String(SecondaryModule); - String freememId = "freemem-" + String(SecondaryModule); - createDiscovery("sensor", //set Type - subjectSYStoMQTTSecondaryModule, freememName.c_str(), (char*)getUniqueId(freememId, "").c_str(), //set state_topic,name,uniqueId - will_Topic, "data_size", "{{ value_json.freemem }}", //set availability_topic,device_class,value_template, - "", "", "B", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - String restartName = "SYS: Restart " + String(SecondaryModule); - String restartId = "restart-" + String(SecondaryModule); - createDiscovery("button", //set Type - will_Topic, restartName.c_str(), (char*)getUniqueId(restartId, "").c_str(), //set state_topic,name,uniqueId - will_Topic, "restart", "", //set availability_topic,device_class,value_template, - "{\"cmd\":\"restart\"}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSsetSecondaryModule, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - createDiscovery("binary_sensor", //set Type - will_Topic, "SYS: Connectivity", (char*)getUniqueId("connectivity", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "connectivity", "", //set availability_topic,device_class,value_template, - Gateway_AnnouncementMsg, will_Message, "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Uptime", (char*)getUniqueId("uptime", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "duration", "{{ value_json.uptime }}", //set availability_topic,device_class,value_template, - "", "", "s", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Free memory", (char*)getUniqueId("freemem", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "data_size", "{{ value_json.freemem }}", //set availability_topic,device_class,value_template, - "", "", "B", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: IP", (char*)getUniqueId("ip", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.ip }}", //set availability_topic,device_class,value_template, - "", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("switch", //set Type - subjectSYStoMQTT, "SYS: Auto discovery", (char*)getUniqueId("disc", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.disc }}", //set availability_topic,device_class,value_template, - "{\"disc\":true,\"save\":true}", "{\"disc\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_avalaible,payload_not avalaible ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain, - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); -# ifdef LED_ADDRESSABLE - createDiscovery("number", //set Type - subjectSYStoMQTT, "SYS: LED Brightness", (char*)getUniqueId("rgbb", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ (value_json.rgbb/2.55) | round(0) }}", //set availability_topic,device_class,value_template, - "{\"rgbb\":{{ (value*2.55) | round(0) }},\"save\":true}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, - stateClassNone //State Class - ); -# endif - -# ifdef ZdisplaySSD1306 -# include "config_SSD1306.h" - createDiscovery("switch", //set Type - subjectSSD1306toMQTT, "SSD1306: Control", (char*)getUniqueId("onstate", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.onstate }}", //set availability_topic,device_class,value_template, - "{\"onstate\":true,\"save\":true}", "{\"onstate\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSSD1306set, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - createDiscovery("switch", //set Type - subjectWebUItoMQTT, "SSD1306: Display metric", (char*)getUniqueId("displayMetric", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.displayMetric }}", //set availability_topic,device_class,value_template, - "{\"displayMetric\":true,\"save\":true}", "{\"displayMetric\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoWebUIset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - createDiscovery("number", //set Type - subjectSSD1306toMQTT, "SSD1306: Brightness", (char*)getUniqueId("brightness", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.brightness }}", //set availability_topic,device_class,value_template, - "{\"brightness\":{{value}},\"save\":true}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSSD1306set, //set,payload_available,payload_not available,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifndef ESP32_ETHERNET - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: RSSI", (char*)getUniqueId("rssi", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "signal_strength", "{{ value_json.rssi }}", //set availability_topic,device_class,value_template, - "", "", "dB", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif -# if defined(ESP32) && !defined(NO_INT_TEMP_READING) - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Internal temperature", (char*)getUniqueId("tempc", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "temperature", "{{ value_json.tempc | round(1)}}", //set availability_topic,device_class,value_template, - "", "", "°C", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_avalaible,payload_not avalaible ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC - stateClassMeasurement //State Class - ); -# if defined(ZboardM5STICKC) || defined(ZboardM5STICKCP) || defined(ZboardM5TOUGH) - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Bat voltage", (char*)getUniqueId("m5batvoltage", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "voltage", "{{ value_json.m5batvoltage }}", //set availability_topic,device_class,value_template, - "", "", "V", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Bat current", (char*)getUniqueId("m5batcurrent", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "current", "{{ value_json.m5batcurrent }}", //set availability_topic,device_class,value_template, - "", "", "A", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Vin voltage", (char*)getUniqueId("m5vinvoltage", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "voltage", "{{ value_json.m5vinvoltage }}", //set availability_topic,device_class,value_template, - "", "", "V", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Vin current", (char*)getUniqueId("m5vincurrent", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "current", "{{ value_json.m5vincurrent }}", //set availability_topic,device_class,value_template, - "", "", "A", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif -# ifdef ZboardM5STACK - createDiscovery("sensor", //set Type - subjectSYStoMQTT, "SYS: Batt level", (char*)getUniqueId("m5battlevel", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "battery", "{{ value_json.m5battlevel }}", //set availability_topic,device_class,value_template, - "", "", "%", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("binary_sensor", //set Type - subjectSYStoMQTT, "SYS: Is Charging", (char*)getUniqueId("m5ischarging", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "{{ value_json.m5ischarging }}", "", //set availability_topic,device_class,value_template, - "", "", "%", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("binary_sensor", //set Type - subjectSYStoMQTT, "SYS: Is Charge Full", (char*)getUniqueId("m5ischargefull", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "{{ value_json.m5ischargefull }}", "", //set availability_topic,device_class,value_template, - "", "", "%", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif -# endif - createDiscovery("button", //set Type - will_Topic, "SYS: Restart gateway", (char*)getUniqueId("restart", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "restart", "", //set availability_topic,device_class,value_template, - "{\"cmd\":\"restart\"}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("button", //set Type - will_Topic, "SYS: Erase credentials", (char*)getUniqueId("erase", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "", //set availability_topic,device_class,value_template, - "{\"cmd\":\"erase\"}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# ifdef MQTT_HTTPS_FW_UPDATE - createDiscovery("update", //set Type - subjectRLStoMQTT, "SYS: Firmware Update", (char*)getUniqueId("update", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "firmware", "", //set availability_topic,device_class,value_template, - LATEST_OR_DEV, "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSupdate, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZsensorBME280 -# include "config_BME280.h" -# define BMEparametersCount 5 - Log.trace(F("bme280Discovery" CR)); - char* BMEsensor[BMEparametersCount][8] = { - {"sensor", "temp", "bme", "temperature", jsonTempc, "", "", "°C"}, - {"sensor", "pa", "bme", "pressure", jsonPa, "", "", "hPa"}, - {"sensor", "hum", "bme", "humidity", jsonHum, "", "", "%"}, - {"sensor", "altim", "bme", "", jsonAltim, "", "", "m"}, - {"sensor", "altift", "bme", "", jsonAltif, "", "", "ft"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < BMEparametersCount; i++) { - createDiscovery(BMEsensor[i][0], - BMETOPIC, BMEsensor[i][1], (char*)getUniqueId(BMEsensor[i][1], BMEsensor[i][2]).c_str(), - will_Topic, BMEsensor[i][3], BMEsensor[i][4], - BMEsensor[i][5], BMEsensor[i][6], BMEsensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - } -# endif - -# ifdef ZsensorHTU21 -# include "config_HTU21.h" -# define HTUparametersCount 2 - Log.trace(F("htu21Discovery" CR)); - char* HTUsensor[HTUparametersCount][8] = { - {"sensor", "temp", "htu", "temperature", jsonTempc, "", "", "°C"}, - {"sensor", "hum", "htu", "humidity", jsonHum, "", "", "%"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < HTUparametersCount; i++) { - //trc(HTUsensor[i][1]); - createDiscovery(HTUsensor[i][0], - HTUTOPIC, HTUsensor[i][1], (char*)getUniqueId(HTUsensor[i][1], HTUsensor[i][2]).c_str(), - will_Topic, HTUsensor[i][3], HTUsensor[i][4], - HTUsensor[i][5], HTUsensor[i][6], HTUsensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorLM75 - Log.trace(F("LM75Discovery" CR)); - char* LM75sensor[8] = {"sensor", "temp", "htu", "temperature", jsonTempc, "", "", "°C"}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - createDiscovery(LM75sensor[0], - LM75TOPIC, LM75sensor[1], (char*)getUniqueId(LM75sensor[1], LM75sensor[2]).c_str(), - will_Topic, LM75sensor[3], LM75sensor[4], - LM75sensor[5], LM75sensor[6], LM75sensor[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); -# endif - -# ifdef ZsensorAHTx0 -# include "config_AHTx0.h" -# define AHTparametersCount 2 - Log.trace(F("AHTx0Discovery" CR)); - char* AHTsensor[AHTparametersCount][8] = { - {"sensor", "temp", "aht", "temperature", jsonTempc, "", "", "°C"}, - {"sensor", "hum", "aht", "humidity", jsonHum, "", "", "%"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < AHTparametersCount; i++) { - createDiscovery(AHTsensor[i][0], - AHTTOPIC, AHTsensor[i][1], (char*)getUniqueId(AHTsensor[i][1], AHTsensor[i][2]).c_str(), - will_Topic, AHTsensor[i][3], AHTsensor[i][4], - AHTsensor[i][5], AHTsensor[i][6], AHTsensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorDHT -# include "config_DHT.h" -# define DHTparametersCount 2 - Log.trace(F("DHTDiscovery" CR)); - char* DHTsensor[DHTparametersCount][8] = { - {"sensor", "temp", "dht", "temperature", jsonTempc, "", "", "°C"}, - {"sensor", "hum", "dht", "humidity", jsonHum, "", "", "%"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < DHTparametersCount; i++) { - //trc(DHTsensor[i][1]); - createDiscovery(DHTsensor[i][0], - DHTTOPIC, DHTsensor[i][1], (char*)getUniqueId(DHTsensor[i][1], DHTsensor[i][2]).c_str(), - will_Topic, DHTsensor[i][3], DHTsensor[i][4], - DHTsensor[i][5], DHTsensor[i][6], DHTsensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorADC -# include "config_ADC.h" - - Log.trace(F("ADCDiscovery" CR)); - char* ADCsensor[8] = {"sensor", "adc", "", "", jsonAdc, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(ADCsensor[1]); - createDiscovery(ADCsensor[0], - ADCTOPIC, ADCsensor[1], (char*)getUniqueId(ADCsensor[1], ADCsensor[2]).c_str(), - will_Topic, ADCsensor[3], ADCsensor[4], - ADCsensor[5], ADCsensor[6], ADCsensor[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZsensorBH1750 -# include "config_BH1750.h" -# define BH1750parametersCount 3 - Log.trace(F("BH1750Discovery" CR)); - char* BH1750sensor[BH1750parametersCount][8] = { - {"sensor", "lux", "BH1750", "illuminance", jsonLux, "", "", "lx"}, - {"sensor", "ftCd", "BH1750", "irradiance", jsonFtcd, "", "", ""}, - {"sensor", "wattsm2", "BH1750", "irradiance", jsonWm2, "", "", "wm²"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < BH1750parametersCount; i++) { - //trc(BH1750sensor[i][1]); - createDiscovery(BH1750sensor[i][0], - subjectBH1750toMQTT, BH1750sensor[i][1], (char*)getUniqueId(BH1750sensor[i][1], BH1750sensor[i][2]).c_str(), - will_Topic, BH1750sensor[i][3], BH1750sensor[i][4], - BH1750sensor[i][5], BH1750sensor[i][6], BH1750sensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorMQ2 -# include "config_MQ2.h" -# define MQ2parametersCount 2 - Log.trace(F("MQ2Discovery" CR)); - char* MQ2sensor[MQ2parametersCount][8] = { - {"sensor", "gas", "MQ2", "gas", jsonVal, "", "", "ppm"}, - {"binary_sensor", "MQ2", "", "gas", jsonPresence, "true", "false", ""} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < MQ2parametersCount; i++) { - createDiscovery(MQ2sensor[i][0], - subjectMQ2toMQTT, MQ2sensor[i][1], (char*)getUniqueId(MQ2sensor[i][1], MQ2sensor[i][2]).c_str(), - will_Topic, MQ2sensor[i][3], MQ2sensor[i][4], - MQ2sensor[i][5], MQ2sensor[i][6], MQ2sensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - } -# endif - -# ifdef ZsensorTEMT6000 -# include "config_TEMT6000.h" -# define TEMT6000parametersCount 3 - Log.trace(F("TEMT6000Discovery" CR)); - char* TEMT6000sensor[TEMT6000parametersCount][8] = { - {"sensor", "lux", "TEMT6000", "illuminance", jsonLux, "", "", "lx"}, - {"sensor", "ftcd", "TEMT6000", "irradiance", jsonFtcd, "", "", ""}, - {"sensor", "wattsm2", "TEMT6000", "irradiance", jsonWm2, "", "", "wm²"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < TEMT6000parametersCount; i++) { - //trc(TEMT6000sensor[i][1]); - createDiscovery(TEMT6000sensor[i][0], - subjectTEMT6000toMQTT, TEMT6000sensor[i][1], (char*)getUniqueId(TEMT6000sensor[i][1], TEMT6000sensor[i][2]).c_str(), - will_Topic, TEMT6000sensor[i][3], TEMT6000sensor[i][4], - TEMT6000sensor[i][5], TEMT6000sensor[i][6], TEMT6000sensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorTSL2561 -# include "config_TSL2561.h" -# define TSL2561parametersCount 3 - Log.trace(F("TSL2561Discovery" CR)); - char* TSL2561sensor[TSL2561parametersCount][8] = { - {"sensor", "lux", "TSL2561", "illuminance", jsonLux, "", "", "lx"}, - {"sensor", "ftcd", "TSL2561", "irradiance", jsonFtcd, "", "", ""}, - {"sensor", "wattsm2", "TSL2561", "irradiance", jsonWm2, "", "", "wm²"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < TSL2561parametersCount; i++) { - //trc(TSL2561sensor[i][1]); - createDiscovery(TSL2561sensor[i][0], - subjectTSL12561toMQTT, TSL2561sensor[i][1], (char*)getUniqueId(TSL2561sensor[i][1], TSL2561sensor[i][2]).c_str(), - will_Topic, TSL2561sensor[i][3], TSL2561sensor[i][4], - TSL2561sensor[i][5], TSL2561sensor[i][6], TSL2561sensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorHCSR501 -# include "config_HCSR501.h" - Log.trace(F("HCSR501Discovery" CR)); - char* HCSR501sensor[8] = {"binary_sensor", "hcsr501", "", "motion", jsonPresence, "true", "false", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(HCSR501sensor[1]); - createDiscovery(HCSR501sensor[0], - subjectHCSR501toMQTT, HCSR501sensor[1], (char*)getUniqueId(HCSR501sensor[1], HCSR501sensor[2]).c_str(), - will_Topic, HCSR501sensor[3], HCSR501sensor[4], - HCSR501sensor[5], HCSR501sensor[6], HCSR501sensor[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZsensorGPIOInput -# include "config_GPIOInput.h" - Log.trace(F("GPIOInputDiscovery" CR)); - char* GPIOInputsensor[8] = {"binary_sensor", "GPIOInput", "", "", jsonGpio, INPUT_GPIO_ON_VALUE, INPUT_GPIO_OFF_VALUE, ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(GPIOInputsensor[1]); - createDiscovery(GPIOInputsensor[0], - subjectGPIOInputtoMQTT, GPIOInputsensor[1], (char*)getUniqueId(GPIOInputsensor[1], GPIOInputsensor[2]).c_str(), - will_Topic, GPIOInputsensor[3], GPIOInputsensor[4], - GPIOInputsensor[5], GPIOInputsensor[6], GPIOInputsensor[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZsensorINA226 -# include "config_INA226.h" -# define INA226parametersCount 3 - Log.trace(F("INA226Discovery" CR)); - char* INA226sensor[INA226parametersCount][8] = { - {"sensor", "volt", "INA226", "voltage", jsonVolt, "", "", "V"}, - {"sensor", "current", "INA226", "current", jsonCurrent, "", "", "A"}, - {"sensor", "power", "INA226", "power", jsonPower, "", "", "W"} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < INA226parametersCount; i++) { - //trc(INA226sensor[i][1]); - createDiscovery(INA226sensor[i][0], - subjectINA226toMQTT, INA226sensor[i][1], (char*)getUniqueId(INA226sensor[i][1], INA226sensor[i][2]).c_str(), - will_Topic, INA226sensor[i][3], INA226sensor[i][4], - INA226sensor[i][5], INA226sensor[i][6], INA226sensor[i][7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -# ifdef ZsensorDS1820 - extern void pubOneWire_HADiscovery(); - // Publish any DS1820 sensors found on the OneWire bus - pubOneWire_HADiscovery(); -# endif - -# ifdef ZactuatorONOFF -# include "config_ONOFF.h" - Log.trace(F("actuatorONOFFDiscovery" CR)); - char* actuatorONOFF[8] = {"switch", "actuatorONOFF", "", "", "{{ value_json.cmd }}", "{\"cmd\":1}", "{\"cmd\":0}", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(actuatorONOFF[1]); - createDiscovery(actuatorONOFF[0], - subjectGTWONOFFtoMQTT, actuatorONOFF[1], (char*)getUniqueId(actuatorONOFF[1], actuatorONOFF[2]).c_str(), - will_Topic, actuatorONOFF[3], actuatorONOFF[4], - actuatorONOFF[5], actuatorONOFF[6], actuatorONOFF[7], - 0, Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoONOFF, - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone, //State Class - "0", "1" //state_off, state_on - ); -# endif - -# ifdef ZsensorRN8209 -# include "config_RN8209.h" -# define RN8209parametersCount 4 - Log.trace(F("RN8209Discovery" CR)); - char* RN8209sensor[RN8209parametersCount][8] = { - {"sensor", "volt", "RN8209", "voltage", jsonVolt, "", "", "V"}, - {"sensor", "current", "RN8209", "current", jsonCurrent, "", "", "A"}, - {"sensor", "power", "RN8209", "power", jsonPower, "", "", "W"}, - {"binary_sensor", "inUse", "RN8209", "power", jsonInuseRN8209, "on", "off", ""} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - }; - - for (int i = 0; i < RN8209parametersCount; i++) { - String name = "NRG: " + String(RN8209sensor[i][1]); - createDiscovery(RN8209sensor[i][0], - subjectRN8209toMQTT, (char*)name.c_str(), (char*)getUniqueId(RN8209sensor[i][1], RN8209sensor[i][2]).c_str(), //set state_topic,name,uniqueId - will_Topic, RN8209sensor[i][3], RN8209sensor[i][4], //set availability_topic,device_class,value_template, - RN8209sensor[i][5], RN8209sensor[i][6], RN8209sensor[i][7], //set,payload_on,payload_off,unit_of_meas - 0, Gateway_AnnouncementMsg, will_Message, true, "", //set off_delay,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassMeasurement //State Class - ); - } -# endif - -// in addition to the MQTT Device Discovery -# if defined(ZgatewayRF) && defined(RF_on_HAS_as_MQTTSensor) - // Sensor to display RF received value - Log.trace(F("gatewayRFDiscovery" CR)); - char* gatewayRF[8] = {"sensor", "gatewayRF", "", "", jsonVal, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewayRF[1]); - createDiscovery(gatewayRF[0], -# if valueAsATopic - subjectRFtoMQTTvalueAsATopic, -# else - subjectRFtoMQTT, -# endif - gatewayRF[1], (char*)getUniqueId(gatewayRF[1], gatewayRF[2]).c_str(), - will_Topic, gatewayRF[3], gatewayRF[4], - gatewayRF[5], gatewayRF[6], gatewayRF[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - -# endif - -# ifdef ZgatewayRF2 -# include "config_RF.h" - - // Sensor to display RF received value - Log.trace(F("gatewayRF2Discovery" CR)); - char* gatewayRF2[8] = {"sensor", "gatewayRF2", "", "", jsonAddress, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewayRF2[1]); - createDiscovery(gatewayRF2[0], -# if valueAsATopic - subjectRF2toMQTTvalueAsATopic, -# else - subjectRF2toMQTT, -# endif - gatewayRF2[1], (char*)getUniqueId(gatewayRF2[1], gatewayRF2[2]).c_str(), - will_Topic, gatewayRF2[3], gatewayRF2[4], - gatewayRF2[5], gatewayRF2[6], gatewayRF2[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZgatewayRFM69 -# include "config_RFM69.h" - // Sensor to display RF received value - Log.trace(F("gatewayRFM69Discovery" CR)); - char* gatewayRFM69[8] = {"sensor", "gatewayRFM69", "", "", jsonVal, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewayRFM69[1]); - createDiscovery(gatewayRFM69[0], - subjectRFM69toMQTT, gatewayRFM69[1], (char*)getUniqueId(gatewayRFM69[1], gatewayRFM69[2]).c_str(), - will_Topic, gatewayRFM69[3], gatewayRFM69[4], - gatewayRFM69[5], gatewayRFM69[6], gatewayRFM69[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZgatewayLORA -# include "config_LORA.h" - // Sensor to display RF received value - Log.trace(F("gatewayLORADiscovery" CR)); - char* gatewayLORA[8] = {"sensor", "gatewayLORA", "", "", jsonMsg, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewayLORA[1]); - createDiscovery(gatewayLORA[0], - subjectLORAtoMQTT, gatewayLORA[1], (char*)getUniqueId(gatewayLORA[1], gatewayLORA[2]).c_str(), - will_Topic, gatewayLORA[3], gatewayLORA[4], - gatewayLORA[5], gatewayLORA[6], gatewayLORA[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - - createDiscovery("switch", //set Type - subjectLORAtoMQTT, "LORA: CRC", (char*)getUniqueId("enablecrc", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.enablecrc }}", //set availability_topic,device_class,value_template, - "{\"enablecrc\":true,\"save\":true}", "{\"enablecrc\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoLORAset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - - createDiscovery("switch", //set Type - subjectLORAtoMQTT, "LORA: Invert IQ", (char*)getUniqueId("invertiq", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.invertiq }}", //set availability_topic,device_class,value_template, - "{\"invertiq\":true,\"save\":true}", "{\"invertiq\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoLORAset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - - createDiscovery("switch", //set Type - subjectLORAtoMQTT, "LORA: Only Known", (char*)getUniqueId("onlyknown", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.onlyknown }}", //set availability_topic,device_class,value_template, - "{\"onlyknown\":true,\"save\":true}", "{\"onlyknown\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoLORAset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); -# endif - -# ifdef ZgatewaySRFB -# include "config_SRFB.h" - // Sensor to display RF received value - Log.trace(F("gatewaySRFBDiscovery" CR)); - char* gatewaySRFB[8] = {"sensor", "gatewaySRFB", "", "", jsonVal, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewaySRFB[1]); - createDiscovery(gatewaySRFB[0], - subjectSRFBtoMQTT, gatewaySRFB[1], (char*)getUniqueId(gatewaySRFB[1], gatewaySRFB[2]).c_str(), - will_Topic, gatewaySRFB[3], gatewaySRFB[4], - gatewaySRFB[5], gatewaySRFB[6], gatewaySRFB[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZgatewayPilight -# include "config_RF.h" - - // Sensor to display RF received value - Log.trace(F("gatewayPilightDiscovery" CR)); - char* gatewayPilight[8] = {"sensor", "gatewayPilight", "", "", jsonMsg, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewayPilight[1]); - createDiscovery(gatewayPilight[0], -# if valueAsATopic - subjectPilighttoMQTTvalueAsATopic, -# else - subjectPilighttoMQTT, -# endif - gatewayPilight[1], (char*)getUniqueId(gatewayPilight[1], gatewayPilight[2]).c_str(), - will_Topic, gatewayPilight[3], gatewayPilight[4], - gatewayPilight[5], gatewayPilight[6], gatewayPilight[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef ZgatewayIR -# include "config_IR.h" - // Sensor to display IR received value - Log.trace(F("gatewayIRDiscovery" CR)); - char* gatewayIR[8] = {"sensor", "gatewayIR", "", "", jsonVal, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gatewayIR[1]); - createDiscovery(gatewayIR[0], - subjectIRtoMQTT, gatewayIR[1], (char*)getUniqueId(gatewayIR[1], gatewayIR[2]).c_str(), - will_Topic, gatewayIR[3], gatewayIR[4], - gatewayIR[5], gatewayIR[6], gatewayIR[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# ifdef Zgateway2G -# include "config_2G.h" - // Sensor to display 2G received value - Log.trace(F("gateway2GDiscovery" CR)); - char* gateway2G[8] = {"sensor", "gateway2G", "", "", jsonMsg, "", "", ""}; - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement - - //trc(gateway2G[1]); - createDiscovery(gateway2G[0], - subject2GtoMQTT, gateway2G[1], (char*)getUniqueId(gateway2G[1], gateway2G[2]).c_str(), - will_Topic, gateway2G[3], gateway2G[4], - gateway2G[5], gateway2G[6], gateway2G[7], - 0, Gateway_AnnouncementMsg, will_Message, true, "", - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); -# endif - -# if defined(ZgatewayBT) || defined(SecondaryModule) -# ifdef ESP32 - - createDiscovery("number", //set Type - subjectBTtoMQTT, "BT: Connect interval", (char*)getUniqueId("intervalcnct", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.intervalcnct/60000 }}", //set availability_topic,device_class,value_template, - "{\"intervalcnct\":{{value*60000}},\"save\":true}", "", "min", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("number", //set Type - subjectBTtoMQTT, "BT: Scan duration", (char*)getUniqueId("scanduration", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.scanduration/1000 }}", //set availability_topic,device_class,value_template, - "{\"scanduration\":{{value*1000}},\"save\":true}", "", "s", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("button", //set Type - will_Topic, "BT: Force scan", (char*)getUniqueId("force_scan", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "", //set availability_topic,device_class,value_template, - "{\"interval\":0}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain - stateClassNone //State Class - ); - createDiscovery("button", //set Type - will_Topic, "BT: Erase config", (char*)getUniqueId("erase_bt_config", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "", //set availability_topic,device_class,value_template, - "{\"erase\":true}", "", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone //State Class - ); - createDiscovery("switch", //set Type - subjectBTtoMQTT, "BT: Publish only sensors", (char*)getUniqueId("only_sensors", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.onlysensors }}", //set availability_topic,device_class,value_template, - "{\"onlysensors\":true,\"save\":true}", "{\"onlysensors\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - createDiscovery("switch", //set Type - subjectBTtoMQTT, "BT: Adaptive scan", (char*)getUniqueId("adaptive_scan", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.adaptivescan }}", //set availability_topic,device_class,value_template, - "{\"adaptivescan\":true,\"save\":true}", "{\"adaptivescan\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - createDiscovery("switch", //set Type - subjectBTtoMQTT, "BT: Enabled", (char*)getUniqueId("enabled", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.enabled }}", //set availability_topic,device_class,value_template, - "{\"enabled\":true,\"save\":true}", "{\"enabled\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - -# define EntitiesCount 9 - const char* obsoleteEntities[EntitiesCount][2] = { - // Remove previously created entities for version < 1.4.0 - {"switch", "active_scan"}, // Replaced by adaptive scan - // Remove previously created entities for version < 1.3.0 - {"number", "scanbcnct"}, // Now a connect interval - // Remove previously created entities for version < 1.2.0 - {"switch", "restart"}, // Now a button - {"switch", "erase"}, // Now a button - {"switch", "force_scan"}, // Now a button - {"sensor", "interval"}, // Now a number - {"sensor", "scanbcnct"}, // Now a number - {"switch", "ohdiscovery"}, // Now a new key - {"switch", "discovery"}}; // Now a new key - - for (int i = 0; i < EntitiesCount; i++) { - eraseTopic(obsoleteEntities[i][0], (char*)getUniqueId(obsoleteEntities[i][1], "").c_str()); - } - - btScanParametersDiscovery(); - - btPresenceParametersDiscovery(); - - createDiscovery("switch", //set Type - subjectBTtoMQTT, "BT: Publish HASS presence", (char*)getUniqueId("hasspresence", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.hasspresence }}", //set availability_topic,device_class,value_template, - "{\"hasspresence\":true,\"save\":true}", "{\"hasspresence\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - createDiscovery("switch", //set Type - subjectBTtoMQTT, "BT: Publish Advertisement data", (char*)getUniqueId("pubadvdata", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.pubadvdata }}", //set availability_topic,device_class,value_template, - "{\"pubadvdata\":true,\"save\":true}", "{\"pubadvdata\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); - createDiscovery("switch", //set Type - subjectBTtoMQTT, "BT: Connect to devices", (char*)getUniqueId("bleconnect", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.bleconnect }}", //set availability_topic,device_class,value_template, - "{\"bleconnect\":true,\"save\":true}", "{\"bleconnect\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic - "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); -# if DEFAULT_LOW_POWER_MODE != DEACTIVATED - createDiscovery("switch", //set Type - subjectSYStoMQTT, "SYS: Low Power Mode command", (char*)getUniqueId("powermode", "").c_str(), //set state_topic,name,uniqueId - will_Topic, "", "{{ value_json.powermode | bool }}", //set availability_topic,device_class,value_template, - "{\"powermode\":1,\"save\":true}", "{\"powermode\":0,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, - 0, //set off_delay - Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available,is a gateway entity, command topic - "", "", "", "", true, // device name, device manufacturer, device model, device MAC, retain - stateClassNone, //State Class - "false", "true" //state_off, state_on - ); -# else - // Remove previously created switch for version < 1.4.0 - eraseTopic("switch", (char*)getUniqueId("powermode", "").c_str()); -# endif -# endif -# endif -} -#else -void pubMqttDiscovery() {} -#endif +/* + OpenMQTTGateway Addon - ESP8266 or Arduino program for home automation + + Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker + Send and receiving command by MQTT + + This is the Home Assistant MQTT Discovery addon. + + Copyright: (c) Rafal Herok + + This file is part of OpenMQTTGateway. + + OpenMQTTGateway is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenMQTTGateway is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "User_config.h" + +#ifdef ZmqttDiscovery +# include "TheengsCommon.h" + +# ifdef ESP8266 +# include +# elif defined(ESP32) +# include + +# include "esp_mac.h" +# endif +# ifdef ESP32_ETHERNET +# include +# endif +# include "config_mqttDiscovery.h" + +extern bool ethConnected; +extern JsonArray modules; + +char discovery_prefix[parameters_size + 1] = discovery_Prefix; +// From https://github.com/home-assistant/core/blob/d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf/homeassistant/const.py#L225 +// List of classes available in Home Assistant +const char* availableHASSClasses[] = {"battery_charging", + "battery", + "carbon_dioxide", + "carbon_monoxide", + "connectivity", + "current", + "data_size", + "distance", + "door", + "duration", + "energy", + "enum", + "frequency", + "gas", + "humidity", + "illuminance", + "irradiance", + "lock", + "motion", + "moving", + "occupancy", + "pm1", + "pm10", + "pm25", + "power_factor", + "power", + "precipitation_intensity", + "precipitation", + "pressure", + "problem", + "restart", + "signal_strength", + "sound_pressure", + "temperature", + "timestamp", + "voltage", + "water", + "weight", + "wind_speed", + "window"}; + +// From https://github.com/home-assistant/core/blob/d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf/homeassistant/const.py#L379 +// List of units available in Home Assistant +const char* availableHASSUnits[] = {"A", + "B", + "UV index", + "V", + "W", + "W", + "bpm", + "bar", + "cm", + "dB", + "dBm", + "ft", + "h", + "hPa", + "Hz", + "kg", + "kW", + "kWh", + "km/h", + "lb", + "lx", + "m/s", + "m/s²", + "m³", + "mg/m³", + "min", + "mm", + "mm/h", + "ms", + "mV", + "µS/cm", + "μg/m³", + "Ω", + "%", + "°", + "°C", + "°F", + "s", + "wb²" +}; + +String getMacAddress() { + uint8_t baseMac[6]; + char baseMacChr[13] = {0}; +# if defined(ESP8266) + WiFi.macAddress(baseMac); + sprintf(baseMacChr, "%02X%02X%02X%02X%02X%02X", baseMac[0], baseMac[1], baseMac[2], baseMac[3], baseMac[4], baseMac[5]); +# elif defined(ESP32) + esp_read_mac(baseMac, ESP_MAC_WIFI_STA); + sprintf(baseMacChr, "%02X%02X%02X%02X%02X%02X", baseMac[0], baseMac[1], baseMac[2], baseMac[3], baseMac[4], baseMac[5]); +# else + sprintf(baseMacChr, "%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +# endif + return String(baseMacChr); +} + +String getUniqueId(String name, String sufix) { + String uniqueId = (String)getMacAddress() + "-" + name + sufix; + return String(uniqueId); +} + +# if defined(ZgatewayBT) || defined(SecondaryModule) +# include "config_BT.h" +/** + * Create a discover messages form a list of attribute + * + * @param mac the MAC address + * @param sensorList[][0] = component type + * @param sensorList[][1] = name + * @param sensorList[][2] = availability topic + * @param sensorList[][3] = device class + * @param sensorList[][4] = value template + * @param sensorList[][5] = payload on + * @param sensorList[][6] = payload off + * @param sensorList[][7] = unit of measurement + * @param sensorList[][8] = unit of measurement + * @param sensorCount number of sensor + * @param device_name name of sensors + * @param device_manufacturer name of manufacturer + * @param device_model the model + * */ +void createDiscoveryFromList(const char* mac, + const char* sensorList[][9], + int sensorCount, + const char* device_name, + const char* device_manufacturer, + const char* device_model) { + for (int i = 0; i < sensorCount; i++) { + String discovery_topic = String(subjectBTtoMQTT) + "/" + String(mac); + String unique_id = String(mac) + "-" + sensorList[i][1]; + + createDiscovery(sensorList[i][0], + discovery_topic.c_str(), sensorList[i][1], unique_id.c_str(), + will_Topic, sensorList[i][3], sensorList[i][4], + sensorList[i][5], sensorList[i][6], sensorList[i][7], + 0, "", "", false, "", + device_name, device_manufacturer, device_model, mac, false, + sensorList[i][8] //The state class + ); + } +} +# endif + +/** + * @brief Announce that the Gateway have the ability to raise Trigger. + * This function provide the configuration of the MQTT Device trigger ( @see https://www.home-assistant.io/integrations/device_trigger.mqtt/ ). + * All messages published by this function will be interpreted as configuration messages of Gateway Triggers. + * Instead, all messages published on the "triggerTopic" will be interpreted as Gateway trigger. + * + * @param triggerTopic Mandatory - The MQTT topic subscribed to receive trigger events. + * @param type The type of the trigger, e.g. button_short_press. Entries supported by the HA Frontend: button_short_press, button_short_release, button_long_press, button_long_release, button_double_press, button_triple_press, button_quadruple_press, button_quintuple_press. If set to an unsupported value, will render as subtype type, e.g. button_1 spammed with type set to spammed and subtype set to button_1 + * @param subtype The subtype of the trigger, e.g. button_1. Entries supported by the HA frontend: turn_on, turn_off, button_1, button_2, button_3, button_4, button_5, button_6. If set to an unsupported value, will render as subtype type, e.g. left_button pressed with type set to button_short_press and subtype set to left_button + * @param object_id The object_id of the trigger. + * @param value_template The template to render the value of the trigger. The template can use the variables trigger.id, trigger.type, trigger.subtype, trigger.payload, trigger.payload_json, trigger.topic, trigger.timestamp, trigger.value, trigger.value_json. The template can be a string or a JSON object. If the template is a JSON object, it must be a valid JSON object. If the template is a string, it will be rendered as a string. If the template is a JSON object, it will be rendered as a JSON object. + */ +void announceGatewayTrigger(const char* triggerTopic, + const char* type, + const char* subtype, + const char* object_id, + const char* value_template) { + //Create The Json + StaticJsonDocument jsonBuffer; + JsonObject sensor = jsonBuffer.to(); + + /** + * The type of automation, must be ‘trigger’. + * @see https://www.home-assistant.io/integrations/device_trigger.mqtt/#automation_type + */ + sensor["automation_type"] = "trigger"; + + /** + * Must be device_automation. Only allowed and required in MQTT auto discovery device messages. + * @see https://www.home-assistant.io/integrations/device_trigger.mqtt/#platform + * @see https://www.home-assistant.io/integrations/mqtt/#device-discovery-payload + */ + sensor["platform "] = "device_automation"; + + // The MQTT topic subscribed to receive trigger events. + if (triggerTopic && triggerTopic[0]) { + char state_topic[mqtt_topic_max_size]; + + strcpy(state_topic, mqtt_topic); + strcat(state_topic, gateway_name); + strcat(state_topic, triggerTopic); + + /** + * "info_topic" is not a standard field, for the message is required the filed "topic", but this filed is reserved and it is used to know where to publish the topic. + * If we want to send on the message the topic information is usefull to use this "info_topic" that will be not delete by the send function but converted to "topic" + */ + sensor["info_topic"] = state_topic; + } else { + Log.error(F("[RF] Error: topic is mandatory for device trigger Discovery" CR)); + return; + } + + /** + * The type of the trigger, e.g. button_short_press. + * Entries supported by the HA frontend: button_short_press, button_short_release, button_long_press, button_long_release, button_double_press, button_triple_press, button_quadruple_press, button_quintuple_press. + * If set to an unsupported value, will render as subtype type, e.g. button_1 spammed with type set to spammed and subtype set to button_1 + */ + if (type && type[0] != 0) { + sensor["type"] = type; + } else { + sensor["type"] = "button_short_press"; + } + + /** + * The subtype of the trigger, e.g. turn_on. + * Entries supported by the frontend: turn_on, turn_off, button_1, button_2, button_3, button_4, button_5, button_6. + * If set to an unsupported value, will render as subtype type, e.g. left_button pressed with type set to button_short_press and subtype set to left_button + */ + if (subtype && subtype[0] != 0) { + sensor["subtype"] = subtype; + } else { + sensor["subtype"] = "turn_on"; + } + + // ------------------ START DEVICE DECLARATION -------------------------------------------------- + // TODO: This section, like the almost identical one in createDiscovery, should be placed in a + // separate function and managed specifically to avoid errors in representing the device + // in the HASS world. + // ------------------------------------------------------------------------------------------------- + + // Information about the device: this device trigger is a part of to tie it into the HA device registry. + StaticJsonDocument jsonDeviceBuffer; + JsonObject device = jsonDeviceBuffer.to(); + + // A link to the webpage that can manage the configuration of this device. + if (ethConnected) { +# ifdef ESP32_ETHERNET + device["configuration_url"] = String("http://") + String(ETH.localIP().toString()) + String("/"); //configuration_url +# endif + } else { + device["configuration_url"] = String("http://") + String(WiFi.localIP().toString()) + String("/"); //configuration_url + } + + /* + * A list of connections of the device to the outside world as a list of tuples [connection_type, connection_identifier]. + * For example the MAC address of a network interface: "connections": [["mac", "02:5b:26:a8:dc:12"]]. + */ + JsonArray connections = device.createNestedArray("connections"); + JsonArray connection_mac = connections.createNestedArray(); + connection_mac.add("mac"); + connection_mac.add(getMacAddress()); + + // A list of IDs that uniquely identify the device. For example a serial number. + String unique_id = String(getMacAddress()); + JsonArray identifiers = device.createNestedArray("identifiers"); + identifiers.add(unique_id); + + // The manufacturer of the device. + device["mf"] = GATEWAY_MANUFACTURER; + + // The model of the device. +# ifndef GATEWAY_MODEL + String model = ""; + serializeJson(modules, model); + device["mdl"] = model; +# else + device["mdl"] = GATEWAY_MODEL; +# endif + + // The name of the device. + device["name"] = String(gateway_name); + device["sw"] = OMG_VERSION; + // ------------------ END DEVICE DECLARATION ------------------ // + + sensor["device"] = device; //device representing the board + + if (value_template && value_template[0]) { + sensor["value_template"] = String(value_template); + } + + /* Publish on the topic + The discovery topic needs to be: /device_automation/[/]/config. + + Note that only one trigger may be defined per unique discovery topic. + Also note that the combination of type and subtype should be unique for a device. + + */ + + String topic_to_publish = String(discovery_prefix) + "/device_automation/" + String(unique_id) + "/" + object_id + "/config"; + Log.trace(F("Announce Gatewy Trigger %s" CR), topic_to_publish.c_str()); + sensor["topic"] = topic_to_publish; + sensor["retain"] = true; + enqueueJsonObject(sensor); +} + +/* + * Remove a substring p from a given string s +*/ +std::string remove_substring(std::string s, const std::string& p) { + std::string::size_type n = p.length(); + + for (std::string::size_type i = s.find(p); + i != std::string::npos; + i = s.find(p)) + s.erase(i, n); + + return s; +} + +/** + * @brief Generate message and publish it on an MQTT discovery explorer. For HA @see https://www.home-assistant.io/docs/mqtt/discovery/ + * + * @param sensor_type the Type + * @param st_topic set state topic, + * @param s_name set name, + * @param unique_id set uniqueId + * @param availability_topic set availability_topic, + * @param device_class set device_class, + * @param value_template set value_template, + * @param payload_on set payload_on, + * @param payload_off set payload_off, + * @param unit_of_meas set unit_of_meas, + * @param off_delay set off_delay + * @param payload_available set payload_available, + * @param payload_not_available set payload_not_available + * @param gateway_entity set is a gateway entity, + * @param cmd_topic set command topic + * @param device_name set device name, + * @param device_manufacturer set device manufacturer, + * @param device_model set device model, + * @param device_id set device(BLE)/entity(RTL_433) identification, + * @param retainCmd set retain + * @param state_class set state class + * + * */ +void createDiscovery(const char* sensor_type, + const char* st_topic, const char* s_name, const char* unique_id, + const char* availability_topic, const char* device_class, const char* value_template, + const char* payload_on, const char* payload_off, const char* unit_of_meas, + int off_delay, + const char* payload_available, const char* payload_not_available, bool gateway_entity, const char* cmd_topic, + const char* device_name, const char* device_manufacturer, const char* device_model, const char* device_id, bool retainCmd, + const char* state_class, const char* state_off, const char* state_on, const char* enum_options, const char* command_template) { + StaticJsonDocument jsonBuffer; + JsonObject sensor = jsonBuffer.to(); + + // If a component cannot render it's state (f.i. KAKU relays) no state topic + // should be added. Without a state topic HA will use optimistic mode for the + // component by default. The Home Assistant UI for optimistic switches + // (separate on and off icons) allows for multiple + // subsequent on commands. This is required for dimming on KAKU relays like + // the ACM-300. + if (st_topic && st_topic[0]) { + char state_topic[mqtt_topic_max_size]; + // If not an entity belonging to the gateway we put wild card for the location and gateway name + // allowing to have the entity detected by several gateways and a consistent discovery topic among the gateways + if (gateway_entity) { + strcpy(state_topic, mqtt_topic); + strcat(state_topic, gateway_name); + } else { + strcpy(state_topic, "+/+"); + } + strcat(state_topic, st_topic); + if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { + sensor["tilt_status_t"] = state_topic; // tilt_status_topic for blind + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { + sensor["pos_t"] = state_topic; // position_topic for curtain + } else { + sensor["stat_t"] = state_topic; + } + } + + if (availability_topic && availability_topic[0] && gateway_entity) { + char avty_topic[mqtt_topic_max_size]; + strcpy(avty_topic, mqtt_topic); + strcat(avty_topic, gateway_name); + strcat(avty_topic, availability_topic); + sensor["avty_t"] = avty_topic; + } + + if (device_class && device_class[0]) { + // We check if the class belongs to HAAS classes list + int num_classes = sizeof(availableHASSClasses) / sizeof(availableHASSClasses[0]); + for (int i = 0; i < num_classes; i++) { // see class list and size into config_mqttDiscovery.h + if (strcmp(availableHASSClasses[i], device_class) == 0) { + sensor["dev_cla"] = device_class; //device_class + } + } + } + + if (unit_of_meas && unit_of_meas[0]) { + // We check if the class belongs to HAAS units list + int num_units = sizeof(availableHASSUnits) / sizeof(availableHASSUnits[0]); + for (int i = 0; i < num_units; i++) { // see units list and size into config_mqttDiscovery.h + if (strcmp(availableHASSUnits[i], unit_of_meas) == 0) { + sensor["unit_of_meas"] = unit_of_meas; //unit_of_measurement*/ + } + } + } + sensor["name"] = s_name; //name + sensor["uniq_id"] = unique_id; //unique_id + if (retainCmd) + sensor["retain"] = retainCmd; // Retain command + if (value_template && value_template[0]) { + if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { + sensor["tilt_status_tpl"] = value_template; // tilt_status_template for blind + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { + sensor["pos_tpl"] = value_template; // position_template for curtain + } else { + sensor["val_tpl"] = value_template; //HA Auto discovery + } + } + if (payload_on && payload_on[0]) { + if (strcmp(sensor_type, "button") == 0) { + sensor["pl_prs"] = payload_on; // payload_press for a button press + } else if (strcmp(sensor_type, "number") == 0) { + sensor["cmd_tpl"] = payload_on; // payload_on for a switch + } else if (strcmp(sensor_type, "update") == 0) { + sensor["pl_inst"] = payload_on; // payload_install for update + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { + int value = std::stoi(payload_on); + sensor["tilt_opnd_val"] = value; // tilt_open_value for blind + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { + int value = std::stoi(payload_on); + sensor["pos_open"] = value; // open value for curtain + } else { + if (strcmp(payload_on, "True") == 0 || strcmp(payload_on, "true") == 0) { + sensor["pl_on"] = true; + } else { + sensor["pl_on"] = payload_on; // payload_on for the rest + } + } + } + if (payload_off && payload_off[0]) { + if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { + sensor["pl_cls"] = payload_off; // payload_close for cover + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { + int value = std::stoi(payload_off); + sensor["pos_clsd"] = value; // closed value for curtain + } else { + if (strcmp(payload_off, "False") == 0 || strcmp(payload_off, "false") == 0) { + sensor["pl_off"] = false; + } else { + sensor["pl_off"] = payload_off; //payload_off for the rest + } + } + } + if (command_template && command_template[0]) { + if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { + sensor["tilt_cmd_tpl"] = command_template; //command_template + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { + sensor["set_pos_tpl"] = command_template; //command_template + } else { + sensor["cmd_tpl"] = command_template; //command_template + } + } + if (strcmp(sensor_type, "device_tracker") == 0) + sensor["source_type"] = "bluetooth_le"; // payload_install for update + if (off_delay != 0) + sensor["off_delay"] = off_delay; //off_delay + if (payload_available[0]) + sensor["pl_avail"] = payload_available; // payload_on + if (payload_not_available[0]) + sensor["pl_not_avail"] = payload_not_available; //payload_off + if (state_class && state_class[0]) + sensor["stat_cla"] = state_class; //add the state class on the sensors ( https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes ) + if (state_on != nullptr) + if (strcmp(state_on, "true") == 0) { + sensor["stat_on"] = true; + } else { + sensor["stat_on"] = state_on; + } + if (state_off != nullptr) + if (strcmp(state_off, "false") == 0) { + sensor["stat_off"] = false; + } else { + sensor["stat_off"] = state_off; + } + if (cmd_topic[0]) { + char command_topic[mqtt_topic_max_size]; + strcpy(command_topic, mqtt_topic); + strcat(command_topic, gateway_name); + strcat(command_topic, cmd_topic); + if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "blind") == 0) { + sensor["tilt_cmd_t"] = command_topic; // tilt_command_topic for cover + } else if (strcmp(sensor_type, "cover") == 0 && strcmp(state_class, "curtain") == 0) { + sensor["set_pos_t"] = command_topic; // position_command_topic for curtain + } else { + sensor["cmd_t"] = command_topic; //command_topic + } + } + + if (enum_options != nullptr) { + sensor["options"] = enum_options; + } + + StaticJsonDocument jsonDeviceBuffer; + JsonObject device = jsonDeviceBuffer.to(); + JsonArray identifiers = device.createNestedArray("ids"); + + if (gateway_entity) { + //device representing the board + device["name"] = String(gateway_name); +# ifndef GATEWAY_MODEL + String model = ""; + serializeJson(modules, model); + device["mdl"] = model; +# else + device["mdl"] = GATEWAY_MODEL; +# endif + device["mf"] = GATEWAY_MANUFACTURER; + if (ethConnected) { +# ifdef ESP32_ETHERNET + device["cu"] = String("http://") + String(ETH.localIP().toString()) + String("/"); //configuration_url +# endif + } else { + device["cu"] = String("http://") + String(WiFi.localIP().toString()) + String("/"); //configuration_url + } + + device["sw"] = OMG_VERSION; + identifiers.add(String(getMacAddress())); + } else { + //The Connections + if (device_id[0]) { + JsonArray connections = device.createNestedArray("cns"); + JsonArray connection_mac = connections.createNestedArray(); + connection_mac.add("mac"); + connection_mac.add(device_id); + //Device representing the actual sensor/switch device + //The Device ID + identifiers.add(device_id); + } + + if (device_manufacturer[0]) { + device["mf"] = device_manufacturer; + } + + if (device_model[0]) { + device["mdl"] = device_model; + } + + // generate unique device name by adding the second half of the device_id only if device_name and device_id are different and we don't want to use the BLE name + if (device_name[0]) { + if (strcmp(device_id, device_name) != 0 && device_id[0] && !ForceDeviceName) { + device["name"] = device_name + String("-") + String(device_id + 6); + } else { + device["name"] = device_name; + } + } + + device["via_device"] = String(getMacAddress()); //mac address of the gateway so that the devices link to the gateway + } + + sensor["device"] = device; + + String topic = String(discovery_prefix) + "/" + String(sensor_type) + "/" + String(unique_id) + "/config"; + Log.trace(F("Announce Device %s on %s" CR), String(sensor_type).c_str(), topic.c_str()); + sensor["topic"] = topic; + sensor["retain"] = true; + enqueueJsonObject(sensor); +} + +void eraseTopic(const char* sensor_type, const char* unique_id) { + if (sensor_type == NULL || unique_id == NULL) { + return; + } + String topic = String(discovery_prefix) + "/" + String(sensor_type) + "/" + String(unique_id) + "/config"; + Log.trace(F("Erase entity discovery %s on %s" CR), String(sensor_type).c_str(), topic.c_str()); + pubMQTT((char*)topic.c_str(), "", true); +} + +# if defined(ZgatewayBT) || defined(SecondaryModule) +void btPresenceParametersDiscovery() { + createDiscovery("number", //set Type + subjectBTtoMQTT, "BT: Presence/Tracker timeout", (char*)getUniqueId("presenceawaytimer", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.presenceawaytimer/60000 }}", //set availability_topic,device_class,value_template, + "{\"presenceawaytimer\":{{value*60000}},\"save\":true}", "", "min", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, + stateClassNone //State Class + ); +} +void btScanParametersDiscovery() { + createDiscovery("number", //set Type + subjectBTtoMQTT, "BT: Interval between scans", (char*)getUniqueId("interval", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.interval/1000 }}", //set availability_topic,device_class,value_template, + "{\"interval\":{{value*1000}},\"save\":true}", "", "s", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, + stateClassNone //State Class + ); + createDiscovery("number", //set Type + subjectBTtoMQTT, "BT: Interval between active scans", (char*)getUniqueId("intervalacts", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.intervalacts/1000 }}", //set availability_topic,device_class,value_template, + "{\"intervalacts\":{{value*1000}},\"save\":true}", "", "s", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, + stateClassNone //State Class + ); +} +# endif + +void pubMqttDiscovery() { + Log.trace(F("omgStatusDiscovery" CR)); +# ifdef SecondaryModule + String uptimeName = "SYS: Uptime " + String(SecondaryModule); + String uptimeId = "uptime-" + String(SecondaryModule); + createDiscovery("sensor", //set Type + subjectSYStoMQTTSecondaryModule, uptimeName.c_str(), (char*)getUniqueId(uptimeId, "").c_str(), //set state_topic,name,uniqueId + will_Topic, "duration", "{{ value_json.uptime }}", //set availability_topic,device_class,value_template, + "", "", "s", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + String freememName = "SYS: Free memory " + String(SecondaryModule); + String freememId = "freemem-" + String(SecondaryModule); + createDiscovery("sensor", //set Type + subjectSYStoMQTTSecondaryModule, freememName.c_str(), (char*)getUniqueId(freememId, "").c_str(), //set state_topic,name,uniqueId + will_Topic, "data_size", "{{ value_json.freemem }}", //set availability_topic,device_class,value_template, + "", "", "B", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + String restartName = "SYS: Restart " + String(SecondaryModule); + String restartId = "restart-" + String(SecondaryModule); + createDiscovery("button", //set Type + will_Topic, restartName.c_str(), (char*)getUniqueId(restartId, "").c_str(), //set state_topic,name,uniqueId + will_Topic, "restart", "", //set availability_topic,device_class,value_template, + "{\"cmd\":\"restart\"}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSsetSecondaryModule, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + createDiscovery("binary_sensor", //set Type + will_Topic, "SYS: Connectivity", (char*)getUniqueId("connectivity", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "connectivity", "", //set availability_topic,device_class,value_template, + Gateway_AnnouncementMsg, will_Message, "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Uptime", (char*)getUniqueId("uptime", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "duration", "{{ value_json.uptime }}", //set availability_topic,device_class,value_template, + "", "", "s", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Free memory", (char*)getUniqueId("freemem", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "data_size", "{{ value_json.freemem }}", //set availability_topic,device_class,value_template, + "", "", "B", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: IP", (char*)getUniqueId("ip", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.ip }}", //set availability_topic,device_class,value_template, + "", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("switch", //set Type + subjectSYStoMQTT, "SYS: Auto discovery", (char*)getUniqueId("disc", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.disc }}", //set availability_topic,device_class,value_template, + "{\"disc\":true,\"save\":true}", "{\"disc\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_avalaible,payload_not avalaible ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain, + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); +# ifdef LED_ADDRESSABLE + createDiscovery("number", //set Type + subjectSYStoMQTT, "SYS: LED Brightness", (char*)getUniqueId("rgbb", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ (value_json.rgbb/2.55) | round(0) }}", //set availability_topic,device_class,value_template, + "{\"rgbb\":{{ (value*2.55) | round(0) }},\"save\":true}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain, + stateClassNone //State Class + ); +# endif + +# ifdef ZdisplaySSD1306 +# include "config_SSD1306.h" + createDiscovery("switch", //set Type + subjectSSD1306toMQTT, "SSD1306: Control", (char*)getUniqueId("onstate", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.onstate }}", //set availability_topic,device_class,value_template, + "{\"onstate\":true,\"save\":true}", "{\"onstate\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSSD1306set, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + createDiscovery("switch", //set Type + subjectWebUItoMQTT, "SSD1306: Display metric", (char*)getUniqueId("displayMetric", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.displayMetric }}", //set availability_topic,device_class,value_template, + "{\"displayMetric\":true,\"save\":true}", "{\"displayMetric\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoWebUIset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + createDiscovery("number", //set Type + subjectSSD1306toMQTT, "SSD1306: Brightness", (char*)getUniqueId("brightness", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.brightness }}", //set availability_topic,device_class,value_template, + "{\"brightness\":{{value}},\"save\":true}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSSD1306set, //set,payload_available,payload_not available,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifndef ESP32_ETHERNET + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: RSSI", (char*)getUniqueId("rssi", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "signal_strength", "{{ value_json.rssi }}", //set availability_topic,device_class,value_template, + "", "", "dB", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif +# if defined(ESP32) && !defined(NO_INT_TEMP_READING) + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Internal temperature", (char*)getUniqueId("tempc", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "temperature", "{{ value_json.tempc | round(1)}}", //set availability_topic,device_class,value_template, + "", "", "°C", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_avalaible,payload_not avalaible ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC + stateClassMeasurement //State Class + ); +# if defined(ZboardM5STICKC) || defined(ZboardM5STICKCP) || defined(ZboardM5TOUGH) + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Bat voltage", (char*)getUniqueId("m5batvoltage", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "voltage", "{{ value_json.m5batvoltage }}", //set availability_topic,device_class,value_template, + "", "", "V", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Bat current", (char*)getUniqueId("m5batcurrent", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "current", "{{ value_json.m5batcurrent }}", //set availability_topic,device_class,value_template, + "", "", "A", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Vin voltage", (char*)getUniqueId("m5vinvoltage", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "voltage", "{{ value_json.m5vinvoltage }}", //set availability_topic,device_class,value_template, + "", "", "V", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Vin current", (char*)getUniqueId("m5vincurrent", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "current", "{{ value_json.m5vincurrent }}", //set availability_topic,device_class,value_template, + "", "", "A", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif +# ifdef ZboardM5STACK + createDiscovery("sensor", //set Type + subjectSYStoMQTT, "SYS: Batt level", (char*)getUniqueId("m5battlevel", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "battery", "{{ value_json.m5battlevel }}", //set availability_topic,device_class,value_template, + "", "", "%", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("binary_sensor", //set Type + subjectSYStoMQTT, "SYS: Is Charging", (char*)getUniqueId("m5ischarging", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "{{ value_json.m5ischarging }}", "", //set availability_topic,device_class,value_template, + "", "", "%", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("binary_sensor", //set Type + subjectSYStoMQTT, "SYS: Is Charge Full", (char*)getUniqueId("m5ischargefull", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "{{ value_json.m5ischargefull }}", "", //set availability_topic,device_class,value_template, + "", "", "%", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, "", //set,payload_available,payload_not available ,is a child device, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif +# endif + createDiscovery("button", //set Type + will_Topic, "SYS: Restart gateway", (char*)getUniqueId("restart", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "restart", "", //set availability_topic,device_class,value_template, + "{\"cmd\":\"restart\"}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("button", //set Type + will_Topic, "SYS: Erase credentials", (char*)getUniqueId("erase", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "", //set availability_topic,device_class,value_template, + "{\"cmd\":\"erase\"}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# ifdef MQTT_HTTPS_FW_UPDATE + createDiscovery("update", //set Type + subjectRLStoMQTT, "SYS: Firmware Update", (char*)getUniqueId("update", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "firmware", "", //set availability_topic,device_class,value_template, + LATEST_OR_DEV, "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSupdate, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZsensorBME280 +# include "config_BME280.h" +# define BMEparametersCount 5 + Log.trace(F("bme280Discovery" CR)); + char* BMEsensor[BMEparametersCount][8] = { + {"sensor", "temp", "bme", "temperature", jsonTempc, "", "", "°C"}, + {"sensor", "pa", "bme", "pressure", jsonPa, "", "", "hPa"}, + {"sensor", "hum", "bme", "humidity", jsonHum, "", "", "%"}, + {"sensor", "altim", "bme", "", jsonAltim, "", "", "m"}, + {"sensor", "altift", "bme", "", jsonAltif, "", "", "ft"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < BMEparametersCount; i++) { + createDiscovery(BMEsensor[i][0], + BMETOPIC, BMEsensor[i][1], (char*)getUniqueId(BMEsensor[i][1], BMEsensor[i][2]).c_str(), + will_Topic, BMEsensor[i][3], BMEsensor[i][4], + BMEsensor[i][5], BMEsensor[i][6], BMEsensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + } +# endif + +# ifdef ZsensorHTU21 +# include "config_HTU21.h" +# define HTUparametersCount 2 + Log.trace(F("htu21Discovery" CR)); + char* HTUsensor[HTUparametersCount][8] = { + {"sensor", "temp", "htu", "temperature", jsonTempc, "", "", "°C"}, + {"sensor", "hum", "htu", "humidity", jsonHum, "", "", "%"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < HTUparametersCount; i++) { + //trc(HTUsensor[i][1]); + createDiscovery(HTUsensor[i][0], + HTUTOPIC, HTUsensor[i][1], (char*)getUniqueId(HTUsensor[i][1], HTUsensor[i][2]).c_str(), + will_Topic, HTUsensor[i][3], HTUsensor[i][4], + HTUsensor[i][5], HTUsensor[i][6], HTUsensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorLM75 + Log.trace(F("LM75Discovery" CR)); + char* LM75sensor[8] = {"sensor", "temp", "htu", "temperature", jsonTempc, "", "", "°C"}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + createDiscovery(LM75sensor[0], + LM75TOPIC, LM75sensor[1], (char*)getUniqueId(LM75sensor[1], LM75sensor[2]).c_str(), + will_Topic, LM75sensor[3], LM75sensor[4], + LM75sensor[5], LM75sensor[6], LM75sensor[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); +# endif + +# ifdef ZsensorAHTx0 +# include "config_AHTx0.h" +# define AHTparametersCount 2 + Log.trace(F("AHTx0Discovery" CR)); + char* AHTsensor[AHTparametersCount][8] = { + {"sensor", "temp", "aht", "temperature", jsonTempc, "", "", "°C"}, + {"sensor", "hum", "aht", "humidity", jsonHum, "", "", "%"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < AHTparametersCount; i++) { + createDiscovery(AHTsensor[i][0], + AHTTOPIC, AHTsensor[i][1], (char*)getUniqueId(AHTsensor[i][1], AHTsensor[i][2]).c_str(), + will_Topic, AHTsensor[i][3], AHTsensor[i][4], + AHTsensor[i][5], AHTsensor[i][6], AHTsensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorDHT +# include "config_DHT.h" +# define DHTparametersCount 2 + Log.trace(F("DHTDiscovery" CR)); + char* DHTsensor[DHTparametersCount][8] = { + {"sensor", "temp", "dht", "temperature", jsonTempc, "", "", "°C"}, + {"sensor", "hum", "dht", "humidity", jsonHum, "", "", "%"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < DHTparametersCount; i++) { + //trc(DHTsensor[i][1]); + createDiscovery(DHTsensor[i][0], + DHTTOPIC, DHTsensor[i][1], (char*)getUniqueId(DHTsensor[i][1], DHTsensor[i][2]).c_str(), + will_Topic, DHTsensor[i][3], DHTsensor[i][4], + DHTsensor[i][5], DHTsensor[i][6], DHTsensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorADC +# include "config_ADC.h" + + Log.trace(F("ADCDiscovery" CR)); + char* ADCsensor[8] = {"sensor", "adc", "", "", jsonAdc, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(ADCsensor[1]); + createDiscovery(ADCsensor[0], + ADCTOPIC, ADCsensor[1], (char*)getUniqueId(ADCsensor[1], ADCsensor[2]).c_str(), + will_Topic, ADCsensor[3], ADCsensor[4], + ADCsensor[5], ADCsensor[6], ADCsensor[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZsensorBH1750 +# include "config_BH1750.h" +# define BH1750parametersCount 3 + Log.trace(F("BH1750Discovery" CR)); + char* BH1750sensor[BH1750parametersCount][8] = { + {"sensor", "lux", "BH1750", "illuminance", jsonLux, "", "", "lx"}, + {"sensor", "ftCd", "BH1750", "irradiance", jsonFtcd, "", "", ""}, + {"sensor", "wattsm2", "BH1750", "irradiance", jsonWm2, "", "", "wm²"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < BH1750parametersCount; i++) { + //trc(BH1750sensor[i][1]); + createDiscovery(BH1750sensor[i][0], + subjectBH1750toMQTT, BH1750sensor[i][1], (char*)getUniqueId(BH1750sensor[i][1], BH1750sensor[i][2]).c_str(), + will_Topic, BH1750sensor[i][3], BH1750sensor[i][4], + BH1750sensor[i][5], BH1750sensor[i][6], BH1750sensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorMQ2 +# include "config_MQ2.h" +# define MQ2parametersCount 2 + Log.trace(F("MQ2Discovery" CR)); + char* MQ2sensor[MQ2parametersCount][8] = { + {"sensor", "gas", "MQ2", "gas", jsonVal, "", "", "ppm"}, + {"binary_sensor", "MQ2", "", "gas", jsonPresence, "true", "false", ""} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < MQ2parametersCount; i++) { + createDiscovery(MQ2sensor[i][0], + subjectMQ2toMQTT, MQ2sensor[i][1], (char*)getUniqueId(MQ2sensor[i][1], MQ2sensor[i][2]).c_str(), + will_Topic, MQ2sensor[i][3], MQ2sensor[i][4], + MQ2sensor[i][5], MQ2sensor[i][6], MQ2sensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + } +# endif + +# ifdef ZsensorTEMT6000 +# include "config_TEMT6000.h" +# define TEMT6000parametersCount 3 + Log.trace(F("TEMT6000Discovery" CR)); + char* TEMT6000sensor[TEMT6000parametersCount][8] = { + {"sensor", "lux", "TEMT6000", "illuminance", jsonLux, "", "", "lx"}, + {"sensor", "ftcd", "TEMT6000", "irradiance", jsonFtcd, "", "", ""}, + {"sensor", "wattsm2", "TEMT6000", "irradiance", jsonWm2, "", "", "wm²"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < TEMT6000parametersCount; i++) { + //trc(TEMT6000sensor[i][1]); + createDiscovery(TEMT6000sensor[i][0], + subjectTEMT6000toMQTT, TEMT6000sensor[i][1], (char*)getUniqueId(TEMT6000sensor[i][1], TEMT6000sensor[i][2]).c_str(), + will_Topic, TEMT6000sensor[i][3], TEMT6000sensor[i][4], + TEMT6000sensor[i][5], TEMT6000sensor[i][6], TEMT6000sensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorTSL2561 +# include "config_TSL2561.h" +# define TSL2561parametersCount 3 + Log.trace(F("TSL2561Discovery" CR)); + char* TSL2561sensor[TSL2561parametersCount][8] = { + {"sensor", "lux", "TSL2561", "illuminance", jsonLux, "", "", "lx"}, + {"sensor", "ftcd", "TSL2561", "irradiance", jsonFtcd, "", "", ""}, + {"sensor", "wattsm2", "TSL2561", "irradiance", jsonWm2, "", "", "wm²"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < TSL2561parametersCount; i++) { + //trc(TSL2561sensor[i][1]); + createDiscovery(TSL2561sensor[i][0], + subjectTSL12561toMQTT, TSL2561sensor[i][1], (char*)getUniqueId(TSL2561sensor[i][1], TSL2561sensor[i][2]).c_str(), + will_Topic, TSL2561sensor[i][3], TSL2561sensor[i][4], + TSL2561sensor[i][5], TSL2561sensor[i][6], TSL2561sensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorHCSR501 +# include "config_HCSR501.h" + Log.trace(F("HCSR501Discovery" CR)); + char* HCSR501sensor[8] = {"binary_sensor", "hcsr501", "", "motion", jsonPresence, "true", "false", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(HCSR501sensor[1]); + createDiscovery(HCSR501sensor[0], + subjectHCSR501toMQTT, HCSR501sensor[1], (char*)getUniqueId(HCSR501sensor[1], HCSR501sensor[2]).c_str(), + will_Topic, HCSR501sensor[3], HCSR501sensor[4], + HCSR501sensor[5], HCSR501sensor[6], HCSR501sensor[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZsensorGPIOInput +# include "config_GPIOInput.h" + Log.trace(F("GPIOInputDiscovery" CR)); + char* GPIOInputsensor[8] = {"binary_sensor", "GPIOInput", "", "", jsonGpio, INPUT_GPIO_ON_VALUE, INPUT_GPIO_OFF_VALUE, ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(GPIOInputsensor[1]); + createDiscovery(GPIOInputsensor[0], + subjectGPIOInputtoMQTT, GPIOInputsensor[1], (char*)getUniqueId(GPIOInputsensor[1], GPIOInputsensor[2]).c_str(), + will_Topic, GPIOInputsensor[3], GPIOInputsensor[4], + GPIOInputsensor[5], GPIOInputsensor[6], GPIOInputsensor[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZsensorINA226 +# include "config_INA226.h" +# define INA226parametersCount 3 + Log.trace(F("INA226Discovery" CR)); + char* INA226sensor[INA226parametersCount][8] = { + {"sensor", "volt", "INA226", "voltage", jsonVolt, "", "", "V"}, + {"sensor", "current", "INA226", "current", jsonCurrent, "", "", "A"}, + {"sensor", "power", "INA226", "power", jsonPower, "", "", "W"} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < INA226parametersCount; i++) { + //trc(INA226sensor[i][1]); + createDiscovery(INA226sensor[i][0], + subjectINA226toMQTT, INA226sensor[i][1], (char*)getUniqueId(INA226sensor[i][1], INA226sensor[i][2]).c_str(), + will_Topic, INA226sensor[i][3], INA226sensor[i][4], + INA226sensor[i][5], INA226sensor[i][6], INA226sensor[i][7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +# ifdef ZsensorDS1820 + extern void pubOneWire_HADiscovery(); + // Publish any DS1820 sensors found on the OneWire bus + pubOneWire_HADiscovery(); +# endif + +# ifdef ZactuatorONOFF +# include "config_ONOFF.h" + Log.trace(F("actuatorONOFFDiscovery" CR)); + char* actuatorONOFF[8] = {"switch", "actuatorONOFF", "", "", "{{ value_json.cmd }}", "{\"cmd\":1}", "{\"cmd\":0}", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(actuatorONOFF[1]); + createDiscovery(actuatorONOFF[0], + subjectGTWONOFFtoMQTT, actuatorONOFF[1], (char*)getUniqueId(actuatorONOFF[1], actuatorONOFF[2]).c_str(), + will_Topic, actuatorONOFF[3], actuatorONOFF[4], + actuatorONOFF[5], actuatorONOFF[6], actuatorONOFF[7], + 0, Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoONOFF, + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone, //State Class + "0", "1" //state_off, state_on + ); +# endif + +# ifdef ZsensorRN8209 +# include "config_RN8209.h" +# define RN8209parametersCount 4 + Log.trace(F("RN8209Discovery" CR)); + char* RN8209sensor[RN8209parametersCount][8] = { + {"sensor", "volt", "RN8209", "voltage", jsonVolt, "", "", "V"}, + {"sensor", "current", "RN8209", "current", jsonCurrent, "", "", "A"}, + {"sensor", "power", "RN8209", "power", jsonPower, "", "", "W"}, + {"binary_sensor", "inUse", "RN8209", "power", jsonInuseRN8209, "on", "off", ""} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + for (int i = 0; i < RN8209parametersCount; i++) { + String name = "NRG: " + String(RN8209sensor[i][1]); + createDiscovery(RN8209sensor[i][0], + subjectRN8209toMQTT, (char*)name.c_str(), (char*)getUniqueId(RN8209sensor[i][1], RN8209sensor[i][2]).c_str(), //set state_topic,name,uniqueId + will_Topic, RN8209sensor[i][3], RN8209sensor[i][4], //set availability_topic,device_class,value_template, + RN8209sensor[i][5], RN8209sensor[i][6], RN8209sensor[i][7], //set,payload_on,payload_off,unit_of_meas + 0, Gateway_AnnouncementMsg, will_Message, true, "", //set off_delay,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassMeasurement //State Class + ); + } +# endif + +// in addition to the MQTT Device Discovery +# if defined(ZgatewayRF) && defined(RF_on_HAS_as_MQTTSensor) + // Sensor to display RF received value + Log.trace(F("gatewayRFDiscovery" CR)); + char* gatewayRF[8] = {"sensor", "gatewayRF", "", "", jsonVal, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewayRF[1]); + createDiscovery(gatewayRF[0], +# if valueAsATopic + subjectRFtoMQTTvalueAsATopic, +# else + subjectRFtoMQTT, +# endif + gatewayRF[1], (char*)getUniqueId(gatewayRF[1], gatewayRF[2]).c_str(), + will_Topic, gatewayRF[3], gatewayRF[4], + gatewayRF[5], gatewayRF[6], gatewayRF[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + +# endif + +# ifdef ZgatewayRF2 +# include "config_RF.h" + + // Sensor to display RF received value + Log.trace(F("gatewayRF2Discovery" CR)); + char* gatewayRF2[8] = {"sensor", "gatewayRF2", "", "", jsonAddress, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewayRF2[1]); + createDiscovery(gatewayRF2[0], +# if valueAsATopic + subjectRF2toMQTTvalueAsATopic, +# else + subjectRF2toMQTT, +# endif + gatewayRF2[1], (char*)getUniqueId(gatewayRF2[1], gatewayRF2[2]).c_str(), + will_Topic, gatewayRF2[3], gatewayRF2[4], + gatewayRF2[5], gatewayRF2[6], gatewayRF2[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZgatewayRFM69 +# include "config_RFM69.h" + // Sensor to display RF received value + Log.trace(F("gatewayRFM69Discovery" CR)); + char* gatewayRFM69[8] = {"sensor", "gatewayRFM69", "", "", jsonVal, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewayRFM69[1]); + createDiscovery(gatewayRFM69[0], + subjectRFM69toMQTT, gatewayRFM69[1], (char*)getUniqueId(gatewayRFM69[1], gatewayRFM69[2]).c_str(), + will_Topic, gatewayRFM69[3], gatewayRFM69[4], + gatewayRFM69[5], gatewayRFM69[6], gatewayRFM69[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZgatewayLORA +# include "config_LORA.h" + // Sensor to display RF received value + Log.trace(F("gatewayLORADiscovery" CR)); + char* gatewayLORA[8] = {"sensor", "gatewayLORA", "", "", jsonMsg, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewayLORA[1]); + createDiscovery(gatewayLORA[0], + subjectLORAtoMQTT, gatewayLORA[1], (char*)getUniqueId(gatewayLORA[1], gatewayLORA[2]).c_str(), + will_Topic, gatewayLORA[3], gatewayLORA[4], + gatewayLORA[5], gatewayLORA[6], gatewayLORA[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + + createDiscovery("switch", //set Type + subjectLORAtoMQTT, "LORA: CRC", (char*)getUniqueId("enablecrc", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.enablecrc }}", //set availability_topic,device_class,value_template, + "{\"enablecrc\":true,\"save\":true}", "{\"enablecrc\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoLORAset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + + createDiscovery("switch", //set Type + subjectLORAtoMQTT, "LORA: Invert IQ", (char*)getUniqueId("invertiq", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.invertiq }}", //set availability_topic,device_class,value_template, + "{\"invertiq\":true,\"save\":true}", "{\"invertiq\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoLORAset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + + createDiscovery("switch", //set Type + subjectLORAtoMQTT, "LORA: Only Known", (char*)getUniqueId("onlyknown", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.onlyknown }}", //set availability_topic,device_class,value_template, + "{\"onlyknown\":true,\"save\":true}", "{\"onlyknown\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoLORAset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); +# endif + +# ifdef ZgatewaySRFB +# include "config_SRFB.h" + // Sensor to display RF received value + Log.trace(F("gatewaySRFBDiscovery" CR)); + char* gatewaySRFB[8] = {"sensor", "gatewaySRFB", "", "", jsonVal, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewaySRFB[1]); + createDiscovery(gatewaySRFB[0], + subjectSRFBtoMQTT, gatewaySRFB[1], (char*)getUniqueId(gatewaySRFB[1], gatewaySRFB[2]).c_str(), + will_Topic, gatewaySRFB[3], gatewaySRFB[4], + gatewaySRFB[5], gatewaySRFB[6], gatewaySRFB[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZgatewayPilight +# include "config_RF.h" + + // Sensor to display RF received value + Log.trace(F("gatewayPilightDiscovery" CR)); + char* gatewayPilight[8] = {"sensor", "gatewayPilight", "", "", jsonMsg, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewayPilight[1]); + createDiscovery(gatewayPilight[0], +# if valueAsATopic + subjectPilighttoMQTTvalueAsATopic, +# else + subjectPilighttoMQTT, +# endif + gatewayPilight[1], (char*)getUniqueId(gatewayPilight[1], gatewayPilight[2]).c_str(), + will_Topic, gatewayPilight[3], gatewayPilight[4], + gatewayPilight[5], gatewayPilight[6], gatewayPilight[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef ZgatewayIR +# include "config_IR.h" + // Sensor to display IR received value + Log.trace(F("gatewayIRDiscovery" CR)); + char* gatewayIR[8] = {"sensor", "gatewayIR", "", "", jsonVal, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gatewayIR[1]); + createDiscovery(gatewayIR[0], + subjectIRtoMQTT, gatewayIR[1], (char*)getUniqueId(gatewayIR[1], gatewayIR[2]).c_str(), + will_Topic, gatewayIR[3], gatewayIR[4], + gatewayIR[5], gatewayIR[6], gatewayIR[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# ifdef Zgateway2G +# include "config_2G.h" + // Sensor to display 2G received value + Log.trace(F("gateway2GDiscovery" CR)); + char* gateway2G[8] = {"sensor", "gateway2G", "", "", jsonMsg, "", "", ""}; + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + + //trc(gateway2G[1]); + createDiscovery(gateway2G[0], + subject2GtoMQTT, gateway2G[1], (char*)getUniqueId(gateway2G[1], gateway2G[2]).c_str(), + will_Topic, gateway2G[3], gateway2G[4], + gateway2G[5], gateway2G[6], gateway2G[7], + 0, Gateway_AnnouncementMsg, will_Message, true, "", + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); +# endif + +# if defined(ZgatewayBT) || defined(SecondaryModule) +# ifdef ESP32 + + createDiscovery("number", //set Type + subjectBTtoMQTT, "BT: Connect interval", (char*)getUniqueId("intervalcnct", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.intervalcnct/60000 }}", //set availability_topic,device_class,value_template, + "{\"intervalcnct\":{{value*60000}},\"save\":true}", "", "min", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("number", //set Type + subjectBTtoMQTT, "BT: Scan duration", (char*)getUniqueId("scanduration", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.scanduration/1000 }}", //set availability_topic,device_class,value_template, + "{\"scanduration\":{{value*1000}},\"save\":true}", "", "s", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("button", //set Type + will_Topic, "BT: Force scan", (char*)getUniqueId("force_scan", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "", //set availability_topic,device_class,value_template, + "{\"interval\":0}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device ID, retain + stateClassNone //State Class + ); + createDiscovery("button", //set Type + will_Topic, "BT: Erase config", (char*)getUniqueId("erase_bt_config", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "", //set availability_topic,device_class,value_template, + "{\"erase\":true}", "", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone //State Class + ); + createDiscovery("switch", //set Type + subjectBTtoMQTT, "BT: Publish only sensors", (char*)getUniqueId("only_sensors", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.onlysensors }}", //set availability_topic,device_class,value_template, + "{\"onlysensors\":true,\"save\":true}", "{\"onlysensors\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + createDiscovery("switch", //set Type + subjectBTtoMQTT, "BT: Adaptive scan", (char*)getUniqueId("adaptive_scan", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.adaptivescan }}", //set availability_topic,device_class,value_template, + "{\"adaptivescan\":true,\"save\":true}", "{\"adaptivescan\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + createDiscovery("switch", //set Type + subjectBTtoMQTT, "BT: Enabled", (char*)getUniqueId("enabled", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.enabled }}", //set availability_topic,device_class,value_template, + "{\"enabled\":true,\"save\":true}", "{\"enabled\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + +# define EntitiesCount 9 + const char* obsoleteEntities[EntitiesCount][2] = { + // Remove previously created entities for version < 1.4.0 + {"switch", "active_scan"}, // Replaced by adaptive scan + // Remove previously created entities for version < 1.3.0 + {"number", "scanbcnct"}, // Now a connect interval + // Remove previously created entities for version < 1.2.0 + {"switch", "restart"}, // Now a button + {"switch", "erase"}, // Now a button + {"switch", "force_scan"}, // Now a button + {"sensor", "interval"}, // Now a number + {"sensor", "scanbcnct"}, // Now a number + {"switch", "ohdiscovery"}, // Now a new key + {"switch", "discovery"}}; // Now a new key + + for (int i = 0; i < EntitiesCount; i++) { + eraseTopic(obsoleteEntities[i][0], (char*)getUniqueId(obsoleteEntities[i][1], "").c_str()); + } + + btScanParametersDiscovery(); + + btPresenceParametersDiscovery(); + + createDiscovery("switch", //set Type + subjectBTtoMQTT, "BT: Publish HASS presence", (char*)getUniqueId("hasspresence", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.hasspresence }}", //set availability_topic,device_class,value_template, + "{\"hasspresence\":true,\"save\":true}", "{\"hasspresence\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + createDiscovery("switch", //set Type + subjectBTtoMQTT, "BT: Publish Advertisement data", (char*)getUniqueId("pubadvdata", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.pubadvdata }}", //set availability_topic,device_class,value_template, + "{\"pubadvdata\":true,\"save\":true}", "{\"pubadvdata\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); + createDiscovery("switch", //set Type + subjectBTtoMQTT, "BT: Connect to devices", (char*)getUniqueId("bleconnect", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.bleconnect }}", //set availability_topic,device_class,value_template, + "{\"bleconnect\":true,\"save\":true}", "{\"bleconnect\":false,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoBTset, //set,payload_available,payload_not available ,is a gateway entity, command topic + "", "", "", "", false, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); +# if DEFAULT_LOW_POWER_MODE != DEACTIVATED + createDiscovery("switch", //set Type + subjectSYStoMQTT, "SYS: Low Power Mode command", (char*)getUniqueId("powermode", "").c_str(), //set state_topic,name,uniqueId + will_Topic, "", "{{ value_json.powermode | bool }}", //set availability_topic,device_class,value_template, + "{\"powermode\":1,\"save\":true}", "{\"powermode\":0,\"save\":true}", "", //set,payload_on,payload_off,unit_of_meas, + 0, //set off_delay + Gateway_AnnouncementMsg, will_Message, true, subjectMQTTtoSYSset, //set,payload_available,payload_not available,is a gateway entity, command topic + "", "", "", "", true, // device name, device manufacturer, device model, device MAC, retain + stateClassNone, //State Class + "false", "true" //state_off, state_on + ); +# else + // Remove previously created switch for version < 1.4.0 + eraseTopic("switch", (char*)getUniqueId("powermode", "").c_str()); +# endif +# endif +# endif +} +#else +void pubMqttDiscovery() {} +#endif From 66ee03d45a8ef771359c44948da75e6b41633e52 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 10 Aug 2025 19:38:40 +1200 Subject: [PATCH 22/24] Add support for a generic xxWSD0xMMCDiscovery List --- main/gatewayBT.cpp | 49 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index 77f98539d0..e534cca51f 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -579,6 +579,31 @@ void XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel) { createDiscoveryFromList(mac, XMWSDJ04MMCsensor, XMWSDJ04MMCparametersCount, "XMWSDJ04MMC", "Xiaomi", sensorModel); } +void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) { + Log.trace(F("xxWSD0xMMCDiscovery" CR)); + int xxWSD0xMMCparametersCount = 5; + if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_DECR") != 0) xxWSD0xMMCparametersCount = 5; + if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) xxWSD0xMMCparametersCount = 7; + const char* xxWSD0xMMCsensor[xxWSD0xMMCparametersCount][9] = { + {"sensor", "Battery", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, + {"sensor", "Temperature", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, + {"sensor", "Humidity", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement}, + {"sensor", "RSSI", mac, "signal_strength", jsonRSSI, "", "", "dB", stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement, state class + }; + if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_DECR") != 0) { // Encrypted PVVX don't have Voltage + const char* voltage[9] = {"sensor", "Voltage", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}; + memcpy(&xxWSD0xMMCsensor[4], voltage, sizeof(voltage)); + }; + if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) { + const char* power[9] = {"sensor", "Power", mac, "", jsonPower, "", "", "", stateClassNone}; + memcpy(&xxWSD0xMMCsensor[5], power, sizeof(power)); + const char* open[9] = {"sensor", "Opening", mac, "", jsonOpen, "", "", "", stateClassNone}; + memcpy(&xxWSD0xMMCsensor[6], open, sizeof(open)); + }; + createDiscoveryFromList(mac, xxWSD0xMMCsensor, xxWSD0xMMCparametersCount, name, "Xiaomi", sensorModel); +} + # else void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) {} void MHO_C401Discovery(const char* mac, const char* sensorModel) {} @@ -586,6 +611,7 @@ 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 XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel_id) {} +void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) {} # endif /* @@ -955,11 +981,11 @@ void launchBTDiscovery(bool overrideDiscovery) { Log.trace(F("properties: %s" CR), properties.c_str()); std::string brand = decoder.getTheengAttribute(p->sensorModel_id, "brand"); std::string model = decoder.getTheengAttribute(p->sensorModel_id, "model"); -# if ForceDeviceName - if (p->name[0] != '\0') { - model = p->name; + if (displayDeviceName || ForceDeviceName) { + if (p->name[0] != '\0') { + model = p->name; + } } -# endif std::string model_id = decoder.getTheengAttribute(p->sensorModel_id, "model_id"); // Check for tracker status @@ -996,7 +1022,9 @@ void launchBTDiscovery(bool overrideDiscovery) { model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, stateClassNone); } - if (!properties.empty()) { + if (displayDeviceName && p->sensorModel_id >= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_ATC && p->sensorModel_id <= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_PVVX_BTHOME_2 ) { + xxWSD0xMMCDiscovery(macWOdots.c_str(), p->name, model_id.c_str()); + } else if (!properties.empty()) { StaticJsonDocument jsonBuffer; auto error = deserializeJson(jsonBuffer, properties); if (error) { @@ -1014,7 +1042,12 @@ void launchBTDiscovery(bool overrideDiscovery) { Log.trace(F("Key: %s"), prop.key().c_str()); Log.trace(F("Unit: %s"), prop.value()["unit"].as()); Log.trace(F("Name: %s"), prop.value()["name"].as()); - String entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); + String entity_name = ""; + if (displayDeviceName || ForceDeviceName) { + entity_name = String(model.c_str()) + "-" + String(prop.key().c_str()); + } else { + entity_name = String(model_id.c_str()) + "-" + String(prop.key().c_str()); + } String unique_id = macWOdots + "-" + String(prop.key().c_str()); String value_template = "{{ value_json." + String(prop.key().c_str()) + " | is_defined }}"; if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1 && strcmp(prop.key().c_str(), "state") == 0) { @@ -1423,7 +1456,7 @@ void process_bledata(JsonObject& BLEdata) { BTConfig.intervalActiveScan = MinTimeBtwScan; BTConfig.scanDuration = MinScanDuration; Log.notice(F("Active and continuous scanning required, parameters adapted" CR)); - stateBTMeasures(false); + // stateBTMeasures(false); // Disabling as it casues a segfault } } else if (BLEdata.containsKey("cont") && BTConfig.BLEinterval != MinTimeBtwScan) { if (BLEdata["cont"]) { @@ -1432,7 +1465,7 @@ void process_bledata(JsonObject& BLEdata) { BTConfig.scanDuration = MinScanDuration; } Log.notice(F("Passive continuous scanning required, parameters adapted" CR)); - stateBTMeasures(false); + // stateBTMeasures(false); // Disabling as it casues a segfault } } } From 6b5dd39d891458024485f211200c44095a0a40ec Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Sun, 10 Aug 2025 19:52:40 +1200 Subject: [PATCH 23/24] Add support for a generic xxWSD0xMMCDiscovery List --- main/mqttDiscovery.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/main/mqttDiscovery.cpp b/main/mqttDiscovery.cpp index 9f9b008a6f..c622173f01 100644 --- a/main/mqttDiscovery.cpp +++ b/main/mqttDiscovery.cpp @@ -586,7 +586,11 @@ void createDiscovery(const char* sensor_type, // generate unique device name by adding the second half of the device_id only if device_name and device_id are different and we don't want to use the BLE name if (device_name[0]) { - if (strcmp(device_id, device_name) != 0 && device_id[0] && !ForceDeviceName) { + #if defined(ZwebUI) && defined(ESP32) // displayDeviceName only applies when running with the WebUI and ESP32 + if (strcmp(device_id, device_name) != 0 && device_id[0] && !displayDeviceName) { + #elif !ForceDeviceName // Support ForceDeviceName for esp8266's + if (strcmp(device_id, device_name) != 0 && device_id[0] && !ForceDeviceName) { + #endif device["name"] = device_name + String("-") + String(device_id + 6); } else { device["name"] = device_name; From f0736113c88a3a9aeda0ca1d5dcb092017d94de1 Mon Sep 17 00:00:00 2001 From: Peter Lambrechtsen Date: Wed, 13 Aug 2025 22:04:06 +1200 Subject: [PATCH 24/24] Adjust stack size and disabled code --- main/gatewayBT.cpp | 40 ++++++---------------------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index aeab647df1..efd9fd0523 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -579,31 +579,6 @@ void XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel) { createDiscoveryFromList(mac, XMWSDJ04MMCsensor, XMWSDJ04MMCparametersCount, "XMWSDJ04MMC", "Xiaomi", sensorModel); } -void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) { - Log.trace(F("xxWSD0xMMCDiscovery" CR)); - int xxWSD0xMMCparametersCount = 5; - if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_DECR") != 0) xxWSD0xMMCparametersCount = 5; - if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) xxWSD0xMMCparametersCount = 7; - const char* xxWSD0xMMCsensor[xxWSD0xMMCparametersCount][9] = { - {"sensor", "Battery", mac, "battery", jsonBatt, "", "", "%", stateClassMeasurement}, - {"sensor", "Temperature", mac, "temperature", jsonTempc, "", "", "°C", stateClassMeasurement}, - {"sensor", "Humidity", mac, "humidity", jsonHum, "", "", "%", stateClassMeasurement}, - {"sensor", "RSSI", mac, "signal_strength", jsonRSSI, "", "", "dB", stateClassMeasurement} - //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement, state class - }; - if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_DECR") != 0) { // Encrypted PVVX don't have Voltage - const char* voltage[9] = {"sensor", "Voltage", mac, "voltage", jsonVolt, "", "", "V", stateClassMeasurement}; - memcpy(&xxWSD0xMMCsensor[4], voltage, sizeof(voltage)); - }; - if (strcmp(sensorModel, "LYWSD03MMC/MJWSD05MMC_PVVX_BTHOME") == 0) { - const char* power[9] = {"sensor", "Power", mac, "", jsonPower, "", "", "", stateClassNone}; - memcpy(&xxWSD0xMMCsensor[5], power, sizeof(power)); - const char* open[9] = {"sensor", "Opening", mac, "", jsonOpen, "", "", "", stateClassNone}; - memcpy(&xxWSD0xMMCsensor[6], open, sizeof(open)); - }; - createDiscoveryFromList(mac, xxWSD0xMMCsensor, xxWSD0xMMCparametersCount, name, "Xiaomi", sensorModel); -} - # else void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) {} void MHO_C401Discovery(const char* mac, const char* sensorModel) {} @@ -611,7 +586,6 @@ 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 XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel_id) {} -void xxWSD0xMMCDiscovery(const char* mac, const char* name, const char* sensorModel) {} # endif /* @@ -896,7 +870,7 @@ void setupBTTasksAndBLE() { # if defined(USE_ESP_IDF) || defined(USE_BLUFI) 14500, # else - 9500, /* Stack size in bytes */ + 10500, /* Stack size in bytes */ # endif NULL, /* Task input parameter */ 2, /* Priority of the task (set higher than core task) */ @@ -1022,9 +996,7 @@ void launchBTDiscovery(bool overrideDiscovery) { model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, stateClassNone); } - if (displayDeviceName && p->sensorModel_id >= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_ATC && p->sensorModel_id <= TheengsDecoder::BLE_ID_NUM::LYWSD03MMC_PVVX_BTHOME_2 ) { - xxWSD0xMMCDiscovery(macWOdots.c_str(), p->name, model_id.c_str()); - } else if (!properties.empty()) { + if (!properties.empty()) { StaticJsonDocument jsonBuffer; auto error = deserializeJson(jsonBuffer, properties); if (error) { @@ -1467,16 +1439,16 @@ void process_bledata(JsonObject& BLEdata) { BTConfig.intervalActiveScan = MinTimeBtwScan; BTConfig.scanDuration = MinScanDuration; Log.notice(F("Active and continuous scanning required, parameters adapted" CR)); - // stateBTMeasures(false); // Disabling as it casues a segfault + stateBTMeasures(false); } - } else if (BLEdata.containsKey("cont") && BTConfig.BLEinterval != MinTimeBtwScan) { - if (BLEdata["cont"]) { + } else if (BLEdata.containsKey("acts") && BTConfig.BLEinterval != MinTimeBtwScan) { + if (BLEdata["acts"]) { BTConfig.BLEinterval = MinTimeBtwScan; if ((BLEdata["type"].as()).compare("CTMO") == 0) { BTConfig.scanDuration = MinScanDuration; } Log.notice(F("Passive continuous scanning required, parameters adapted" CR)); - // stateBTMeasures(false); // Disabling as it casues a segfault + stateBTMeasures(false); } } }