Add Switchbot Blind Tilt control (#2002)
close_up, close_down, open, and percentage command Add auto-discovery with HA and cover component
This commit is contained in:
1 parent
4ec577d6af
commit
04c3e7ef95
6 files changed
+244
-22
No files matched your search
+51
-3
@@ -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.
|
||||
|
||||
@@ -82,5 +82,14 @@ public:
|
||||
bool processActions(std::vector<BLEAction>& 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<BLEAction>& actions) override;
|
||||
};
|
||||
|
||||
#endif //ESP32
|
||||
#endif //zBLEConnect_h
|
||||
@@ -500,5 +500,105 @@ bool SBS1_connect::processActions(std::vector<BLEAction>& 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<BLEAction>& 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<JSON_MSG_BUFFER> BLEdataBuffer;
|
||||
JsonObject BLEdata = BLEdataBuffer.to<JsonObject>();
|
||||
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
|
||||
+41
-3
@@ -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<std::string>(); // Fix #1694
|
||||
action.value = val;
|
||||
@@ -1400,6 +1413,31 @@ void MQTTtoBTAction(JsonObject& BTdata) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (BTdata.containsKey("model_id") && BTdata["model_id"].is<const char*>() && 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<int>()) {
|
||||
action.value_type = BLE_VAL_INT;
|
||||
} else if (BTdata.containsKey("tilt") && BTdata["tilt"].is<const char*>()) {
|
||||
action.value_type = BLE_VAL_STRING;
|
||||
} else {
|
||||
Log.error(F("BLE value type invalid" CR));
|
||||
return;
|
||||
}
|
||||
std::string val = BTdata["tilt"].as<std::string>(); // 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<int>() : 0;
|
||||
action.value_type = BLE_VAL_STRING;
|
||||
|
||||
+42
-15
@@ -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<JSON_MSG_BUFFER> 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<JsonObject>();
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -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/
|
||||
|
||||
Reference in new issue
Block a user