diff --git a/docs/use/ble.md b/docs/use/ble.md index b662c35..97eef86 100644 --- a/docs/use/ble.md +++ b/docs/use/ble.md @@ -217,6 +217,29 @@ The `ttl` parameter is the number of attempts to connect (defaults to 1), which `value_type` can be one of: STRING, HEX, INT, FLOAT. Default is STRING if omitted in the message. ::: +## Switchbot S1 control (ESP32 only) + +Switchbot S1 devices are automatically discovered and available as a device in the configuration menu of home assistant. + +::: tip If the switchbot mode is changed the ESP32 must be restarted. ::: + +The device can also be controlled over MQTT with a simplified BLE write command. + +### Example command to set the Switchbot state to ON: +``` +mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoBT/config -m '{ + "SBS1":"on", + "mac":"AA:BB:CC:DD:EE:FF" +}' +``` +Response (assuming success): +``` +{ + "id":"AA:BB:CC:DD:EE:FF", + "state":"on" +} +``` + ## Other To check your hm10 firmware version upload a serial sketch to the nodemcu (this will enable communication directly with the hm10) and launch the command: diff --git a/main/ZgatewayBLEConnect.h b/main/ZgatewayBLEConnect.h index feb9443..556d6f3 100644 --- a/main/ZgatewayBLEConnect.h +++ b/main/ZgatewayBLEConnect.h @@ -66,5 +66,14 @@ public: void publishData() override; }; +class SBS1_connect : public zBLEConnect { + uint8_t m_notifyVal; + void notifyCB(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify); + +public: + SBS1_connect(NimBLEAddress& addr) : zBLEConnect(addr) {} + bool processActions(std::vector& actions) override; +}; + #endif //ESP32 #endif //zBLEConnect_h diff --git a/main/ZgatewayBLEConnect.ino b/main/ZgatewayBLEConnect.ino index cf42ca7..11f1eb3 100644 --- a/main/ZgatewayBLEConnect.ino +++ b/main/ZgatewayBLEConnect.ino @@ -342,5 +342,77 @@ void XMWSDJ04MMC_connect::publishData() { } } +/*-----------------------SBS1 HANDLING-----------------------*/ +void SBS1_connect::notifyCB(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { + if (m_taskHandle == nullptr) { + return; // unexpected notification + } + if (!ProcessLock) { + Log.trace(F("Callback from %s characteristic" CR), pChar->getUUID().toString().c_str()); + + if (length) { + m_notifyVal = *pData; + } else { + Log.notice(F("Invalid notification data" CR)); + return; + } + } else { + Log.trace(F("Callback process canceled by processLock" CR)); + } + + xTaskNotifyGive(m_taskHandle); +} + +bool SBS1_connect::processActions(std::vector& actions) { + NimBLEUUID serviceUUID("cba20d00-224d-11e6-9fb8-0002a5d5c51b"); + NimBLEUUID charUUID("cba20002-224d-11e6-9fb8-0002a5d5c51b"); + NimBLEUUID notifyCharUUID("cba20003-224d-11e6-9fb8-0002a5d5c51b"); + static byte ON[] = {0x57, 0x01, 0x01}; + static byte OFF[] = {0x57, 0x01, 0x02}; + + bool result = false; + if (actions.size() > 0) { + for (auto& it : actions) { + if (NimBLEAddress(it.addr) == m_pClient->getPeerAddress()) { + NimBLERemoteCharacteristic* pChar = getCharacteristic(serviceUUID, charUUID); + NimBLERemoteCharacteristic* pNotifyChar = getCharacteristic(serviceUUID, notifyCharUUID); + + if (it.write && pChar && pNotifyChar) { + Log.trace(F("processing Switchbot %s" CR), it.value.c_str()); + if (pNotifyChar->subscribe(true, + std::bind(&SBS1_connect::notifyCB, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4), + true)) { + if (it.value == "on") { + result = pChar->writeValue(ON, 3, false); + } else { + result = pChar->writeValue(OFF, 3, false); + } + + if (result) { + m_taskHandle = xTaskGetCurrentTaskHandle(); + if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(BLE_CNCT_TIMEOUT)) == pdFALSE) { + m_taskHandle = nullptr; + } + result = m_notifyVal == 0x01; + } + } + } + + it.complete = result; + if (result || it.ttl <= 1) { + JsonObject BLEresult = getBTJsonObject(); + BLEresult["id"] = it.addr; + BLEresult["state"] = result ? it.value : it.value == "on" ? "off" : "on"; + pubBT(BLEresult); + } + } + } + } + + return result; +} + # endif //ZgatewayBT #endif //ESP32 diff --git a/main/ZgatewayBT.ino b/main/ZgatewayBT.ino index f3c8142..eb15891 100644 --- a/main/ZgatewayBT.ino +++ b/main/ZgatewayBT.ino @@ -1,18 +1,18 @@ -/* - OpenMQTTGateway - ESP8266 or Arduino program for home automation +/* + OpenMQTTGateway - ESP8266 or Arduino program for home automation - Act as a wifi or ethernet gateway between your 433mhz/infrared IR signal/BLE and a MQTT broker + Act as a wifi or ethernet gateway between your 433mhz/infrared IR signal/BLE and a MQTT broker Send and receiving command by MQTT - + This gateway enables to: - publish MQTT data to a different topic related to BLE devices rssi signal - publish MQTT data related to mi flora temperature, moisture, fertility and lux - publish MQTT data related to mi jia indoor temperature & humidity sensor 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 @@ -461,7 +461,7 @@ void procBLETask(void* pvParameters) { } } -/** +/** * BLEscan used to retrieve BLE advertized data from devices without connection */ void BLEscan() { @@ -484,7 +484,7 @@ void BLEscan() { Log.trace(F("Process BLE stack free: %u" CR), uxTaskGetStackHighWaterMark(xProcBLETaskHandle)); } -/** +/** * Connect to BLE devices and initiate the callbacks with a service/characteristic request */ void BLEconnect() { @@ -513,6 +513,9 @@ void BLEconnect() { 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 { GENERIC_connect BLEclient(addr); if (BLEclient.processActions(BLEactions)) { @@ -874,13 +877,25 @@ void launchBTDiscovery() { # else String value_template = "{{ value_json." + String(prop.key().c_str()) + " | is_defined }}"; # endif - 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); + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::SBS1 && !strcmp(prop.key().c_str(), "state")) { + String payload_on = "{\"SBS1\":\"on\",\"mac\":\"" + String(p->macAdr) + "\"}"; + String payload_off = "{\"SBS1\":\"off\",\"mac\":\"" + 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, subjectMQTTtoBTset, + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone, "off", "on"); + } else { + 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 && @@ -1001,9 +1016,80 @@ void BTforceScan() { } } +# ifdef ESP32 +void immediateBTAction(void* pvParameters) { + if (BLEactions.size()) { + // Immediate action; we need to prevent the normal connection action and stop scanning + ProcessLock = true; + NimBLEScan* pScan = NimBLEDevice::getScan(); + if (pScan->isScanning()) { + pScan->stop(); + } + + if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { + // swap the vectors so only this device is processed + std::vector dev_swap; + dev_swap.push_back(getDeviceByMac(BLEactions.back().addr)); + 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 + ProcessLock = false; + BLEconnect(); + // back to normal + std::swap(devices, dev_swap); + std::swap(BLEactions, act_swap); + + // If we stopped the scheduled connect for this action, do the scheduled now + if ((!(scanCount % BLEscanBeforeConnect) || scanCount == 1) && bleConnect) + BLEconnect(); + xSemaphoreGive(semaphoreBLEOperation); + } else { + Log.error(F("BLE busy - command not sent" CR)); + JsonObject result = getBTJsonObject(); + result["id"] = BLEactions.back().addr; + result["success"] = false; + pubBT(result); + BLEactions.pop_back(); + ProcessLock = false; + } + } + vTaskDelete(NULL); +} + +void startBTActionTask() { + TaskHandle_t th; + xTaskCreatePinnedToCore( + immediateBTAction, /* Function to implement the task */ + "imActTask", /* Name of the task */ + 5120, /* 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 */ +} +# endif + void MQTTtoBTAction(JsonObject& BTdata) { # ifdef ESP32 BLEAction action; + memset(&action, 0, sizeof(BLEAction)); + if (BTdata.containsKey("SBS1")) { + strcpy(action.addr, (const char*)BTdata["mac"]); + action.write = true; + action.value = BTdata["SBS1"].as(); + action.ttl = 1; + createOrUpdateDevice(action.addr, device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBS1, 1); + BLEactions.push_back(action); + startBTActionTask(); + return; + } + action.ttl = BTdata.containsKey("ttl") ? (uint8_t)BTdata["ttl"] : 1; action.addr_type = BTdata.containsKey("mac_type") ? BTdata["mac_type"].as() : 0; action.value_type = BLE_VAL_STRING; @@ -1050,43 +1136,9 @@ void MQTTtoBTAction(JsonObject& BTdata) { TheengsDecoder::BLE_ID_NUM::UNKNOWN_MODEL, action.addr_type); + BLEactions.push_back(action); if (BTdata.containsKey("immediate") && BTdata["immediate"].as()) { - // Immediate action; we need to prevent the normal connection action and stop scanning - ProcessLock = true; - NimBLEScan* pScan = NimBLEDevice::getScan(); - if (pScan->isScanning()) { - pScan->stop(); - } - - if (xSemaphoreTake(semaphoreBLEOperation, pdMS_TO_TICKS(5000)) == pdTRUE) { - // swap the vectors so only this device is processed - std::vector dev_swap; - dev_swap.push_back(getDeviceByMac(action.addr)); - std::swap(devices, dev_swap); - - std::vector act_swap; - act_swap.push_back(action); - std::swap(BLEactions, act_swap); - - // Unlock here to allow the action to be performed - ProcessLock = false; - BLEconnect(); - // back to normal - std::swap(devices, dev_swap); - std::swap(BLEactions, act_swap); - // If we stopped the scheduled connect for this action, do the scheduled now - if ((!(scanCount % BLEscanBeforeConnect) || scanCount == 1) && bleConnect) - BLEconnect(); - xSemaphoreGive(semaphoreBLEOperation); - } else { - Log.error(F("BLE busy - command not sent" CR)); - JsonObject result = getBTJsonObject(); - result["id"] = action.addr; - result["success"] = false; - pubBT(result); - } - } else { - BLEactions.push_back(action); + startBTActionTask(); } # endif } diff --git a/main/ZmqttDiscovery.ino b/main/ZmqttDiscovery.ino index a6baa86..1dde7ef 100644 --- a/main/ZmqttDiscovery.ino +++ b/main/ZmqttDiscovery.ino @@ -221,7 +221,7 @@ void createDiscovery(const char* sensor_type, int off_delay, const char* payload_available, const char* payload_not_avalaible, bool gateway_entity, const char* cmd_topic, const char* device_name, const char* device_manufacturer, const char* device_model, const char* device_mac, bool retainCmd, - const char* state_class) { + const char* state_class, const char* state_off, const char* state_on) { StaticJsonDocument jsonBuffer; JsonObject sensor = jsonBuffer.to(); @@ -282,6 +282,10 @@ void createDiscovery(const char* sensor_type, sensor["pl_not_avail"] = payload_not_avalaible; //payload_off if (state_class[0]) sensor["state_class"] = 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) + sensor["stat_on"] = state_on; + if (state_off != nullptr) + sensor["stat_off"] = state_off; if (cmd_topic[0]) { char command_topic[mqtt_topic_max_size]; diff --git a/main/config_mqttDiscovery.h b/main/config_mqttDiscovery.h index 2a15243..359066d 100644 --- a/main/config_mqttDiscovery.h +++ b/main/config_mqttDiscovery.h @@ -88,7 +88,7 @@ extern void createDiscovery(const char* sensor_type, int off_delay, const char* payload_available, const char* payload_not_avalaible, bool gateway_entity, const char* command_topic, const char* device_name, const char* device_manufacturer, const char* device_model, const char* device_mac, bool retainCmd, - const char* state_class); + const char* state_class, const char* state_off = nullptr, const char* state_on = nullptr); /** * @brief Create a message for Discovery Device Trigger. For HA @see https://www.home-assistant.io/integrations/device_trigger.mqtt/ diff --git a/platformio.ini b/platformio.ini index fcb5eca..6a1f68f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -212,11 +212,11 @@ board_build.ldscript = eagle.flash.1m64.ld ;this frees more space for firmware u [env:sonoff-rfbridge-direct] platform = espressif8266@^2 board = esp8285 -lib_deps = +lib_deps = ${com-esp.lib_deps} ${libraries.wifimanager8266} ${libraries.esppilight} -build_flags = +build_flags = ${com-esp.build_flags} '-DZgatewayPilight="Pilight"' '-DRF_RECEIVER_GPIO=4' @@ -374,9 +374,9 @@ lib_deps = ${com-esp.lib_deps} ${libraries.wifimanager32} ${libraries.emodbus} - ${libraries.gfSunInverter} + ${libraries.gfSunInverter} build_flags = - ${com-esp.build_flags} + ${com-esp.build_flags} '-DZgatewayGFSunInverter="GFSunInverter"' '-DGateway_Name="OpenMQTTGateway_ESP32_GFSunInverter"' @@ -702,7 +702,7 @@ lib_deps = ${libraries.ble} ${libraries.decoder} ${libraries.irremoteesp} -build_flags = +build_flags = ${com-esp.build_flags} '-DZgatewayBT="BT"' '-DZgatewayIR="IR"' @@ -726,7 +726,7 @@ build_flags = '-DZgatewayRTL_433="rtl_433"' '-DGateway_Name="OpenMQTTGateway_rtl_433_ESP"' '-DvalueAsASubject=true' ; mqtt topic includes model and device -; '-DPUBLISH_UNPARSED=true' ; Publish details of undecoded signals +; '-DPUBLISH_UNPARSED=true' ; Publish details of undecoded signals ; '-DRTL_DEBUG=4' ; enable rtl_433 verbose device decode [env:esp32dev-multi_receiver] @@ -810,7 +810,7 @@ build_flags = '-DZgatewayLORA="LORA"' '-DZgatewayBT="BT"' '-DGateway_Name="OpenMQTTGateway_ESP32_BLE_LORA"' - + '-DLED_SEND_RECEIVE=21' # T-BEAM board V0.5 # '-DLED_SEND_RECEIVE=14' # T-BEAM board V0.7 # '-DLED_SEND_RECEIVE=4' # T-BEAM board V1.0+ @@ -823,7 +823,7 @@ build_flags = '-DADC_GPIO=35' '-DADC_DIVIDER=2' # Reading battery level every minutes should be more than enought - '-DTimeBetweenReadingADC=60000' + '-DTimeBetweenReadingADC=60000' [env:heltec-wifi-lora-32-868] platform = ${com.esp32_platform} @@ -990,7 +990,7 @@ lib_deps = ${libraries.wifimanager8266} ${libraries.irremoteesp} ${libraries.esp8266_mdns} -build_flags = +build_flags = ${com-esp.build_flags} '-DZgatewayIR="IR"' '-DTRIGGER_GPIO=13'