From 04c3e7ef95fc348c7f795025b8d42ed25f323b4e Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 14 Aug 2024 14:55:15 -0500 Subject: [PATCH] Add Switchbot Blind Tilt control (#2002) close_up, close_down, open, and percentage command Add auto-discovery with HA and cover component --- docs/use/ble.md | 54 +++++++++++++++++-- main/ZgatewayBLEConnect.h | 9 ++++ main/ZgatewayBLEConnect.ino | 100 ++++++++++++++++++++++++++++++++++++ main/ZgatewayBT.ino | 44 ++++++++++++++-- main/ZmqttDiscovery.ino | 57 ++++++++++++++------ main/config_mqttDiscovery.h | 2 +- 6 files changed, 244 insertions(+), 22 deletions(-) diff --git a/docs/use/ble.md b/docs/use/ble.md index fd9c7d8..e2e13a7 100644 --- a/docs/use/ble.md +++ b/docs/use/ble.md @@ -375,7 +375,7 @@ The `ttl` parameter is the number of attempts to connect (defaults to 1), which ## SwitchBot Bot control (available with HA discovery) -SwitchBot Bot devices are automatically discovered and available as a device in the configuration menu of home assistant. +SwitchBot Bot devices are automatically discovered and available as a device in the Home Assistant Settings menu. ::: tip If the SwitchBot mode is changed the ESP32 must be restarted. @@ -383,11 +383,11 @@ 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: +### Example command to set the SwitchBot Bot state to ON: ``` mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoBT -m '{ "SBS1":"on", - "mac":"AA:BB:CC:DD:EE:FF" + "id":"AA:BB:CC:DD:EE:FF" }' ``` Response (assuming success): @@ -398,6 +398,54 @@ Response (assuming success): } ``` +## SwitchBot Blind Tilt control (available with HA discovery) + +SwitchBot Blind Tilt devices are automatically discovered and available as a device in the settings Home Assistant Settings menu. The following commands are supported: +* Open +* Close down +* Set a percentage + +The device can also be controlled over MQTT with a simplified BLE write command. + +### Example command to set the SwitchBot Blind Tilt state to OPEN: +mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoBT -m '{ + "model_id":"W270160X", + "tilt":"open", + "id":"AA:BB:CC:DD:EE:FF" + }' + +### Example command to set the SwitchBot Blind Tilt state to CLOSE_DOWN: +mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoBT -m '{ + "model_id":"W270160X", + "tilt":"close_down", + "id":"AA:BB:CC:DD:EE:FF" + }' + +### Example command to set the SwitchBot Blind Tilt state to CLOSE_UP: +mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoBT -m '{ + "model_id":"W270160X", + "tilt":"close_up", + "id":"AA:BB:CC:DD:EE:FF" + }' + +### Example command to set the SwitchBot Blind Tilt state to a percentage (0-100): +mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoBT -m '{ + "model_id":"W270160X", + "tilt":55, + "id":"AA:BB:CC:DD:EE:FF" + }' + +### Response (assuming success): +The Switchbot Blind Tilt response : +``` +{ + "id":"AA:BB:CC:DD:EE:FF", + "tilt":55, + "open":90, + "direction":"up" +} +``` + ## ADVANCED: Setting up an external decoder This advanced option is used to publish raw radio frames on a specific topic to be decoded by an external decoder instead of the integrated one. diff --git a/main/ZgatewayBLEConnect.h b/main/ZgatewayBLEConnect.h index f4d0b30..406cac1 100644 --- a/main/ZgatewayBLEConnect.h +++ b/main/ZgatewayBLEConnect.h @@ -82,5 +82,14 @@ public: bool processActions(std::vector& actions) override; }; +class SBBT_connect : public zBLEConnect { + uint8_t m_notifyVal; + void notifyCB(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify); + +public: + SBBT_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 0af221a..83f59d5 100644 --- a/main/ZgatewayBLEConnect.ino +++ b/main/ZgatewayBLEConnect.ino @@ -500,5 +500,105 @@ bool SBS1_connect::processActions(std::vector& actions) { return result; } +/*-----------------------SBBT HANDLING-----------------------*/ +void SBBT_connect::notifyCB(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { + if (m_taskHandle == nullptr) { + return; // unexpected notification + } + if (!BTProcessLock) { + 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 BTProcessLock" CR)); + } + + xTaskNotifyGive(m_taskHandle); +} + +bool SBBT_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 OPEN[] = {0x57, 0x0f, 0x45, 0x01, 0x01, 0x01, 0x32}; + static byte CLOSE_DOWN[] = {0x57, 0x0f, 0x45, 0x01, 0x01, 0x01, 0x00}; + static byte CLOSE_UP[] = {0x57, 0x0f, 0x45, 0x01, 0x01, 0x01, 0x64}; + static byte MOVE[] = {0x57, 0x0f, 0x45, 0x01, 0x01, 0x01, 0x00}; + + 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); + int value = -1; + if (it.value_type == BLE_VAL_INT) { + value = std::stoi(it.value); + } + if (it.write && pChar && pNotifyChar) { + Log.trace(F("processing Switchbot %s" CR), it.value.c_str()); + if (pNotifyChar->subscribe(true, + std::bind(&SBBT_connect::notifyCB, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4), + true)) { + if (it.value == "open" && it.value_type == BLE_VAL_STRING) { + result = pChar->writeValue(OPEN, 7, false); + value = 50; + } else if (it.value == "close_down" && it.value_type == BLE_VAL_STRING) { + result = pChar->writeValue(CLOSE_DOWN, 7, false); + value = 0; + } else if (it.value == "close_up" && it.value_type == BLE_VAL_STRING) { + result = pChar->writeValue(CLOSE_UP, 7, false); + value = 100; + } else if (it.value_type == BLE_VAL_INT) { + if (value >= 0 && value <= 100) { + byte posByte = (byte)value; + MOVE[6] = posByte; + result = pChar->writeValue(MOVE, 7, 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) { + StaticJsonDocument BLEdataBuffer; + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["id"] = it.addr; + BLEdata["tilt"] = value; + if (value == 50) { + BLEdata["open"] = 100; + BLEdata["direction"] = "-"; + } else if (value < 50) { + BLEdata["open"] = value * 2; + BLEdata["direction"] = "down"; + } else if (value > 50) { + BLEdata["open"] = (100 - value) * 2; + BLEdata["direction"] = "up"; + } + buildTopicFromId(BLEdata, subjectBTtoMQTT); + handleJsonEnqueue(BLEdata, QueueSemaphoreTimeOutTask); + } + } + } + } + + return result; +} + # endif //ZgatewayBT #endif //ESP32 diff --git a/main/ZgatewayBT.ino b/main/ZgatewayBT.ino index 685423c..1124a2e 100644 --- a/main/ZgatewayBT.ino +++ b/main/ZgatewayBT.ino @@ -742,6 +742,9 @@ void BLEconnect() { } 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 { GENERIC_connect BLEclient(addr); if (BLEclient.processActions(BLEactions)) { @@ -1058,8 +1061,8 @@ void launchBTDiscovery(bool overrideDiscovery) { 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 = "{\"SBS1\":\"on\",\"mac\":\"" + String(p->macAdr) + "\"}"; - String payload_off = "{\"SBS1\":\"off\",\"mac\":\"" + String(p->macAdr) + "\"}"; + String payload_on = "{\"SBS1\":\"on\",\"id\":\"" + String(p->macAdr) + "\"}"; + String payload_off = "{\"SBS1\":\"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(), @@ -1067,6 +1070,16 @@ void launchBTDiscovery(bool overrideDiscovery) { Gateway_AnnouncementMsg, will_Message, false, subjectMQTTtoBT, model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, stateClassNone, "off", "on"); + } 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, + stateClassNone, 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", @@ -1388,7 +1401,7 @@ void MQTTtoBTAction(JsonObject& BTdata) { BLEAction action; memset(&action, 0, sizeof(BLEAction)); if (BTdata.containsKey("SBS1")) { - strcpy(action.addr, (const char*)BTdata["mac"]); + strcpy(action.addr, (const char*)BTdata["id"]); action.write = true; std::string val = BTdata["SBS1"].as(); // Fix #1694 action.value = val; @@ -1400,6 +1413,31 @@ void MQTTtoBTAction(JsonObject& BTdata) { return; } + if (BTdata.containsKey("model_id") && BTdata["model_id"].is() && BTdata["model_id"] == "W270160X") { + if (!BTdata.containsKey("id")) { + Log.error(F("BLE mac address missing" CR)); + return; + } + strcpy(action.addr, (const char*)BTdata["id"]); + action.write = true; + if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { + action.value_type = BLE_VAL_INT; + } else if (BTdata.containsKey("tilt") && BTdata["tilt"].is()) { + action.value_type = BLE_VAL_STRING; + } else { + Log.error(F("BLE value type invalid" CR)); + return; + } + std::string val = BTdata["tilt"].as(); // Fix #1694 + action.value = val; + action.ttl = 3; + createOrUpdateDevice(action.addr, device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBBT, 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; diff --git a/main/ZmqttDiscovery.ino b/main/ZmqttDiscovery.ino index b0af0c8..1742e1c 100644 --- a/main/ZmqttDiscovery.ino +++ b/main/ZmqttDiscovery.ino @@ -239,8 +239,8 @@ void createDiscovery(const char* sensor_type, 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) { - StaticJsonDocument jsonBuffer; + const char* state_class, const char* state_off, const char* state_on, const char* enum_options, const char* command_template) { + StaticJsonDocument<1024> jsonBuffer; JsonObject sensor = jsonBuffer.to(); // If a component cannot render it's state (f.i. KAKU relays) no state topic @@ -249,7 +249,7 @@ void createDiscovery(const char* sensor_type, // (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[0]) { + 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 @@ -260,10 +260,14 @@ void createDiscovery(const char* sensor_type, strcpy(state_topic, "+/+"); } strcat(state_topic, st_topic); - sensor["stat_t"] = state_topic; + if (strcmp(sensor_type, "cover") == 0) { + sensor["tilt_status_t"] = state_topic; // tilt_status_topic for cover + } else { + sensor["stat_t"] = state_topic; + } } - if (availability_topic[0] && gateway_entity) { + 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); @@ -271,7 +275,7 @@ void createDiscovery(const char* sensor_type, sensor["avty_t"] = avty_topic; } - if (device_class[0]) { + 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 @@ -281,7 +285,7 @@ void createDiscovery(const char* sensor_type, } } - if (unit_of_meas[0]) { + 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 @@ -294,26 +298,45 @@ void createDiscovery(const char* sensor_type, sensor["uniq_id"] = unique_id; //unique_id if (retainCmd) sensor["retain"] = retainCmd; // Retain command - if (value_template[0]) { + if (value_template && value_template[0]) { if (strstr(value_template, " | is_defined") != NULL && SYSConfig.ohdiscovery) { sensor["val_tpl"] = remove_substring(value_template, " | is_defined"); //OpenHAB compatible HA auto discovery } else { - sensor["val_tpl"] = value_template; //HA Auto discovery + if (strcmp(sensor_type, "cover") == 0) { + sensor["tilt_status_tpl"] = value_template; // tilt_status_template for cover + } else { + sensor["val_tpl"] = value_template; //HA Auto discovery + } } } - if (payload_on[0]) { + 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["payload_install"] = payload_on; // payload_install for update + sensor["pl_inst"] = payload_on; // payload_install for update + } else if (strcmp(sensor_type, "cover") == 0) { + int value = std::stoi(payload_on); + sensor["tilt_opnd_val"] = value; // tilt_open_value for cover } else { sensor["pl_on"] = payload_on; // payload_on for the rest } } - if (payload_off[0]) - sensor["pl_off"] = payload_off; //payload_off + if (payload_off && payload_off[0]) { + if (strcmp(sensor_type, "cover") == 0) { + sensor["pl_cls"] = payload_off; // payload_close for cover + } else { + sensor["pl_off"] = payload_off; //payload_off + } + } + if (command_template && command_template[0]) { + if (strcmp(sensor_type, "cover") == 0) { + sensor["tilt_cmd_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) @@ -323,7 +346,7 @@ void createDiscovery(const char* sensor_type, if (payload_not_available[0]) sensor["pl_not_avail"] = payload_not_available; //payload_off if (state_class && 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 ) + 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; @@ -341,7 +364,11 @@ void createDiscovery(const char* sensor_type, strcpy(command_topic, mqtt_topic); strcat(command_topic, gateway_name); strcat(command_topic, cmd_topic); - sensor["cmd_t"] = command_topic; //command_topic + if (strcmp(sensor_type, "cover") == 0) { + sensor["tilt_cmd_t"] = command_topic; // tilt_command_topic for cover + } else { + sensor["cmd_t"] = command_topic; //command_topic + } } if (enum_options != nullptr) { diff --git a/main/config_mqttDiscovery.h b/main/config_mqttDiscovery.h index 74ffc3d..d9f6322 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_available, 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_off = nullptr, const char* state_on = nullptr, const char* enum_options = nullptr); + const char* state_class, const char* state_off = nullptr, const char* state_on = nullptr, const char* enum_options = nullptr, const char* command_template = nullptr); /** * @brief Create a message for Discovery Device Trigger. For HA @see https://www.home-assistant.io/integrations/device_trigger.mqtt/