From 00fe36f426215328c6347b78f30d7500150d2c76 Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 5 Sep 2024 08:54:53 -0500 Subject: [PATCH] [BT] Add Switchbot curtain (#2049) Add SB curtain commands and add stop to SB tilt Co-authored-by: Florian <1technophile@users.noreply.github.com> --- docs/use/ble.md | 61 +++++++++++++++++++++++++++ main/ZgatewayBLEConnect.ino | 25 +++++++---- main/ZgatewayBT.ino | 82 ++++++++++++++++++++++++++----------- main/ZmqttDiscovery.ino | 32 +++++++++++---- 4 files changed, 160 insertions(+), 40 deletions(-) diff --git a/docs/use/ble.md b/docs/use/ble.md index 92a59e0..ed67db5 100644 --- a/docs/use/ble.md +++ b/docs/use/ble.md @@ -486,6 +486,15 @@ mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoBT -m '{ }' ``` +### Set the SwitchBot Blind Tilt to Stop: +``` +mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoBT -m '{ + "model_id":"W270160X", + "tilt":"stop", + "id":"AA:BB:CC:DD:EE:FF" + }' +``` + ### Response (assuming success): The Switchbot Blind Tilt response : ``` @@ -497,6 +506,58 @@ The Switchbot Blind Tilt response : } ``` +## SwitchBot Curtain 3 control (available with HA discovery) + +SwitchBot Curtain 3 devices are automatically discovered and available as a device in the settings Home Assistant Settings menu. The following commands are supported: +* Set a percentage + +The device can also be controlled over MQTT with a simplified BLE write command. + +### Set the SwitchBot Curtain state to OPEN: +``` +mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoBT -m '{ + "model_id":"W070160X", + "position":"open", + "id":"AA:BB:CC:DD:EE:FF" + }' +``` + +### Set the SwitchBot Curtain state to CLOSE: +``` +mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoBT -m '{ + "model_id":"W070160X", + "position":"close", + "id":"AA:BB:CC:DD:EE:FF" + }' +``` + +### Set the SwitchBot Curtain to a percentage (0-100): +``` +mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoBT -m '{ + "model_id":"W070160X", + "position":55, + "id":"AA:BB:CC:DD:EE:FF" + }' +``` + +### Set the SwitchBot Curtain to Stop: +``` +mosquitto_pub -t home/OpenMQTTGateway/commands/MQTTtoBT -m '{ + "model_id":"W070160X", + "position":"stop", + "id":"AA:BB:CC:DD:EE:FF" + }' +``` + +### Response (assuming success): +The Switchbot Curtain response : +``` +{ + "id":"AA:BB:CC:DD:EE:FF", + "position":55, +} +``` + ## 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.ino b/main/ZgatewayBLEConnect.ino index c3b2e80..2f18f20 100644 --- a/main/ZgatewayBLEConnect.ino +++ b/main/ZgatewayBLEConnect.ino @@ -538,6 +538,7 @@ bool SBBT_connect::processActions(std::vector& actions) { 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}; + static byte STOP[] = {0x57, 0x0f, 0x45, 0x01, 0x00, 0x01}; bool result = false; if (actions.size() > 0) { @@ -545,7 +546,7 @@ bool SBBT_connect::processActions(std::vector& actions) { if (NimBLEAddress(it.addr) == m_pClient->getPeerAddress()) { NimBLERemoteCharacteristic* pChar = getCharacteristic(serviceUUID, charUUID); NimBLERemoteCharacteristic* pNotifyChar = getCharacteristic(serviceUUID, notifyCharUUID); - int value = -1; + int value = -99; if (it.value_type == BLE_VAL_INT) { value = std::stoi(it.value); } @@ -565,12 +566,16 @@ bool SBBT_connect::processActions(std::vector& actions) { } 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 == "stop" && it.value_type == BLE_VAL_STRING) { + result = pChar->writeValue(STOP, 6, false); } 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); } + } else if (value == -1 && it.value_type == BLE_VAL_INT) { + result = pChar->writeValue(STOP, 6, false); } if (result) { @@ -588,14 +593,15 @@ bool SBBT_connect::processActions(std::vector& actions) { StaticJsonDocument BLEdataBuffer; JsonObject BLEdata = BLEdataBuffer.to(); BLEdata["id"] = it.addr; - BLEdata["tilt"] = value; + if (value != -99 || value != -1) + BLEdata["tilt"] = value; if (value == 50) { BLEdata["open"] = 100; BLEdata["direction"] = "-"; - } else if (value < 50) { + } else if (value < 50 && value >= 0) { BLEdata["open"] = value * 2; BLEdata["direction"] = "down"; - } else if (value > 50) { + } else if (value > 50 && value <= 100) { BLEdata["open"] = (100 - value) * 2; BLEdata["direction"] = "up"; } @@ -637,7 +643,7 @@ bool SBCU_connect::processActions(std::vector& actions) { static byte CLOSE[] = {0x57, 0x0f, 0x45, 0x01, 0x01, 0x01, 0x64}; static byte OPEN[] = {0x57, 0x0f, 0x45, 0x01, 0x01, 0x01, 0x00}; static byte MOVE[] = {0x57, 0x0f, 0x45, 0x01, 0x01, 0x01, 0x00}; - static byte STOP[] = {0x57, 0x0f, 0x45, 0x01, 0x01, 0x00, 0x01}; + static byte STOP[] = {0x57, 0x0f, 0x45, 0x01, 0x00, 0x01}; bool result = false; if (actions.size() > 0) { @@ -645,7 +651,7 @@ bool SBCU_connect::processActions(std::vector& actions) { if (NimBLEAddress(it.addr) == m_pClient->getPeerAddress()) { NimBLERemoteCharacteristic* pChar = getCharacteristic(serviceUUID, charUUID); NimBLERemoteCharacteristic* pNotifyChar = getCharacteristic(serviceUUID, notifyCharUUID); - int value = -1; + int value = -99; if (it.value_type == BLE_VAL_INT) { value = std::stoi(it.value); } @@ -663,13 +669,15 @@ bool SBCU_connect::processActions(std::vector& actions) { result = pChar->writeValue(CLOSE, 7, false); value = 0; } else if (it.value == "stop" && it.value_type == BLE_VAL_STRING) { - result = pChar->writeValue(STOP, 7, false); + result = pChar->writeValue(STOP, 6, false); } 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); } + } else if (value == -1 && it.value_type == BLE_VAL_INT) { + result = pChar->writeValue(STOP, 6, false); } if (result) { @@ -687,7 +695,8 @@ bool SBCU_connect::processActions(std::vector& actions) { StaticJsonDocument BLEdataBuffer; JsonObject BLEdata = BLEdataBuffer.to(); BLEdata["id"] = it.addr; - BLEdata["position"] = value; + if (value != -99 || value != -1) + BLEdata["position"] = value; buildTopicFromId(BLEdata, subjectBTtoMQTT); enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); } diff --git a/main/ZgatewayBT.ino b/main/ZgatewayBT.ino index 6604140..1eed175 100644 --- a/main/ZgatewayBT.ino +++ b/main/ZgatewayBT.ino @@ -751,6 +751,9 @@ void BLEconnect() { } 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)) { @@ -1036,7 +1039,16 @@ void launchBTDiscovery(bool overrideDiscovery) { "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()); + "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", @@ -1387,30 +1399,54 @@ void KnownBTActions(JsonObject& BTdata) { startBTActionTask(); 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)); + if (BTdata.containsKey("model_id") && BTdata["model_id"].is()) { + if (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; + } else if (BTdata["model_id"] == "W070160X") { + 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("position") && BTdata["position"].is()) { + action.value_type = BLE_VAL_INT; + } else if (BTdata.containsKey("position") && BTdata["position"].is()) { + action.value_type = BLE_VAL_STRING; + } else { + Log.error(F("BLE value type invalid" CR)); + return; + } + std::string val = BTdata["position"].as(); // Fix #1694 + action.value = val; + action.ttl = 3; + createOrUpdateDevice(action.addr, device_flags_connect, + TheengsDecoder::BLE_ID_NUM::SBCU, 1); + BLEactions.push_back(action); + startBTActionTask(); 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; } } # else diff --git a/main/ZmqttDiscovery.ino b/main/ZmqttDiscovery.ino index 63addc0..4b23c6e 100644 --- a/main/ZmqttDiscovery.ino +++ b/main/ZmqttDiscovery.ino @@ -260,8 +260,10 @@ void createDiscovery(const char* sensor_type, strcpy(state_topic, "+/+"); } strcat(state_topic, st_topic); - if (strcmp(sensor_type, "cover") == 0) { - sensor["tilt_status_t"] = state_topic; // tilt_status_topic for cover + 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; } @@ -302,8 +304,10 @@ void createDiscovery(const char* sensor_type, if (strstr(value_template, " | is_defined") != NULL && SYSConfig.ohdiscovery) { sensor["val_tpl"] = remove_substring(value_template, " | is_defined"); //OpenHAB compatible HA auto discovery } else { - if (strcmp(sensor_type, "cover") == 0) { - sensor["tilt_status_tpl"] = value_template; // tilt_status_template for cover + 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 } @@ -316,23 +320,31 @@ void createDiscovery(const char* sensor_type, 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) { + } 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 cover + 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 { sensor["pl_on"] = payload_on; // payload_on for the rest } } if (payload_off && payload_off[0]) { - if (strcmp(sensor_type, "cover") == 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 { sensor["pl_off"] = payload_off; //payload_off } } if (command_template && command_template[0]) { - if (strcmp(sensor_type, "cover") == 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 } @@ -364,8 +376,10 @@ void createDiscovery(const char* sensor_type, strcpy(command_topic, mqtt_topic); strcat(command_topic, gateway_name); strcat(command_topic, cmd_topic); - if (strcmp(sensor_type, "cover") == 0) { + 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 }