Support for AM43 BLE blind motors (#1744)

Co-authored-by: Ben Buxton <bb@cactii.net>
Co-authored-by: Otto Winter <otto@otto-winter.com>
Co-authored-by: Stefan Agner <stefan@agner.ch>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: René Klomp <rene@klomp.ws>
Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl>
Co-authored-by: Geoff Davis <geoff@geoffdavis.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: dentra <dentra@users.noreply.github.com>
Co-authored-by: Guillermo Ruffino <glm.net@gmail.com>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Barry Loong <loongyh@users.noreply.github.com>
Co-authored-by: Sergey V. DUDANOV <sergey.dudanov@gmail.com>
Co-authored-by: Balazs Scheidler <bazsi77@gmail.com>
This commit is contained in:
buxtronix 2021-08-11 14:07:10 +10:00 committed by GitHub
parent e5366dbbe7
commit 947c104eff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 663 additions and 1 deletions

View File

@ -14,6 +14,8 @@ esphome/core/* @esphome/core
esphome/components/ac_dimmer/* @glmnet
esphome/components/adc/* @esphome/core
esphome/components/addressable_light/* @justfalter
esphome/components/am43/* @buxtronix
esphome/components/am43/cover/* @buxtronix
esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix
esphome/components/api/* @OttoWinter

View File

View File

@ -0,0 +1,115 @@
#include "am43.h"
#include "esphome/core/log.h"
#ifdef ARDUINO_ARCH_ESP32
namespace esphome {
namespace am43 {
static const char *TAG = "am43";
void Am43::dump_config() {
ESP_LOGCONFIG(TAG, "AM43");
LOG_SENSOR(" ", "Battery", this->battery_);
LOG_SENSOR(" ", "Illuminance", this->illuminance_);
}
void Am43::setup() {
this->encoder_ = new Am43Encoder();
this->decoder_ = new Am43Decoder();
this->logged_in_ = false;
this->last_battery_update_ = 0;
this->current_sensor_ = 0;
}
void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_OPEN_EVT: {
this->logged_in_ = false;
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
this->logged_in_ = false;
this->node_state = espbt::ClientState::Idle;
if (this->battery_ != nullptr)
this->battery_->publish_state(NAN);
if (this->illuminance_ != nullptr)
this->illuminance_->publish_state(NAN);
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID);
if (chr == nullptr) {
if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) {
ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.",
this->parent_->address_str().c_str());
} else {
ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?",
this->parent_->address_str().c_str());
}
break;
}
this->char_handle_ = chr->handle;
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
this->node_state = espbt::ClientState::Established;
this->update();
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.handle != this->char_handle_)
break;
this->decoder_->decode(param->notify.value, param->notify.value_len);
if (this->battery_ != nullptr && this->decoder_->has_battery_level() &&
millis() - this->last_battery_update_ > 10000) {
this->battery_->publish_state(this->decoder_->battery_level_);
this->last_battery_update_ = millis();
}
if (this->illuminance_ != nullptr && this->decoder_->has_light_level()) {
this->illuminance_->publish_state(this->decoder_->light_level_);
}
if (this->current_sensor_ > 0) {
if (this->illuminance_ != nullptr) {
auto packet = this->encoder_->get_light_level_request();
auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_,
packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP,
ESP_GATT_AUTH_REQ_NONE);
if (status)
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
status);
}
this->current_sensor_ = 0;
}
break;
}
default:
break;
}
}
void Am43::update() {
if (this->node_state != espbt::ClientState::Established) {
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
return;
}
if (this->current_sensor_ == 0) {
if (this->battery_ != nullptr) {
auto packet = this->encoder_->get_battery_level_request();
auto status =
esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length,
packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status)
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
}
this->current_sensor_++;
}
}
} // namespace am43
} // namespace esphome
#endif

View File

@ -0,0 +1,45 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/am43/am43_base.h"
#ifdef ARDUINO_ARCH_ESP32
#include <esp_gattc_api.h>
namespace esphome {
namespace am43 {
namespace espbt = esphome::esp32_ble_tracker;
class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent {
public:
void setup() override;
void update() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_battery(sensor::Sensor *battery) { battery_ = battery; }
void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; }
protected:
uint16_t char_handle_;
Am43Encoder *encoder_;
Am43Decoder *decoder_;
bool logged_in_;
sensor::Sensor *battery_{nullptr};
sensor::Sensor *illuminance_{nullptr};
uint8_t current_sensor_;
// The AM43 often gets into a state where it spams loads of battery update
// notifications. Here we will limit to no more than every 10s.
uint8_t last_battery_update_;
};
} // namespace am43
} // namespace esphome
#endif

View File

@ -0,0 +1,142 @@
#include "am43_base.h"
namespace esphome {
namespace am43 {
const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a};
std::string pkt_to_hex(const uint8_t *data, uint16_t len) {
char buf[64];
memset(buf, 0, 64);
for (int i = 0; i < len; i++)
sprintf(&buf[i * 2], "%02x", data[i]);
std::string ret = buf;
return ret;
}
Am43Packet *Am43Encoder::get_battery_level_request() {
uint8_t data = 0x1;
return this->encode_(0xA2, &data, 1);
}
Am43Packet *Am43Encoder::get_light_level_request() {
uint8_t data = 0x1;
return this->encode_(0xAA, &data, 1);
}
Am43Packet *Am43Encoder::get_position_request() {
uint8_t data = 0x1;
return this->encode_(CMD_GET_POSITION, &data, 1);
}
Am43Packet *Am43Encoder::get_send_pin_request(uint16_t pin) {
uint8_t data[2];
data[0] = (pin & 0xFF00) >> 8;
data[1] = pin & 0xFF;
return this->encode_(CMD_SEND_PIN, data, 2);
}
Am43Packet *Am43Encoder::get_open_request() {
uint8_t data = 0xDD;
return this->encode_(CMD_SET_STATE, &data, 1);
}
Am43Packet *Am43Encoder::get_close_request() {
uint8_t data = 0xEE;
return this->encode_(CMD_SET_STATE, &data, 1);
}
Am43Packet *Am43Encoder::get_stop_request() {
uint8_t data = 0xCC;
return this->encode_(CMD_SET_STATE, &data, 1);
}
Am43Packet *Am43Encoder::get_set_position_request(uint8_t position) {
return this->encode_(CMD_SET_POSITION, &position, 1);
}
void Am43Encoder::checksum_() {
uint8_t checksum = 0;
int i = 0;
for (i = 0; i < this->packet_.length; i++)
checksum = checksum ^ this->packet_.data[i];
this->packet_.data[i] = checksum ^ 0xff;
this->packet_.length++;
}
Am43Packet *Am43Encoder::encode_(uint8_t command, uint8_t *data, uint8_t length) {
memcpy(this->packet_.data, START_PACKET, 5);
this->packet_.data[5] = command;
this->packet_.data[6] = length;
memcpy(&this->packet_.data[7], data, length);
this->packet_.length = length + 7;
this->checksum_();
ESP_LOGV("am43", "ENC(%d): 0x%s", packet_.length, pkt_to_hex(packet_.data, packet_.length).c_str());
return &this->packet_;
}
#define VERIFY_MIN_LENGTH(x) \
if (length < (x)) \
return;
void Am43Decoder::decode(const uint8_t *data, uint16_t length) {
this->has_battery_level_ = false;
this->has_light_level_ = false;
this->has_set_position_response_ = false;
this->has_set_state_response_ = false;
this->has_position_ = false;
this->has_pin_response_ = false;
ESP_LOGV("am43", "DEC(%d): 0x%s", length, pkt_to_hex(data, length).c_str());
if (length < 2 || data[0] != 0x9a)
return;
switch (data[1]) {
case CMD_GET_BATTERY_LEVEL: {
VERIFY_MIN_LENGTH(8);
this->battery_level_ = data[7];
this->has_battery_level_ = true;
break;
}
case CMD_GET_LIGHT_LEVEL: {
VERIFY_MIN_LENGTH(5);
this->light_level_ = 100 * ((float) data[4] / 9);
this->has_light_level_ = true;
break;
}
case CMD_GET_POSITION: {
VERIFY_MIN_LENGTH(6);
this->position_ = data[5];
this->has_position_ = true;
break;
}
case CMD_NOTIFY_POSITION: {
VERIFY_MIN_LENGTH(5);
this->position_ = data[4];
this->has_position_ = true;
break;
}
case CMD_SEND_PIN: {
VERIFY_MIN_LENGTH(4);
this->pin_ok_ = data[3] == RESPONSE_ACK;
this->has_pin_response_ = true;
break;
}
case CMD_SET_POSITION: {
VERIFY_MIN_LENGTH(4);
this->set_position_ok_ = data[3] == RESPONSE_ACK;
this->has_set_position_response_ = true;
break;
}
case CMD_SET_STATE: {
VERIFY_MIN_LENGTH(4);
this->set_state_ok_ = data[3] == RESPONSE_ACK;
this->has_set_state_response_ = true;
break;
}
default:
break;
}
};
} // namespace am43
} // namespace esphome

View File

@ -0,0 +1,78 @@
#pragma once
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace am43 {
static const uint16_t AM43_SERVICE_UUID = 0xFE50;
static const uint16_t AM43_CHARACTERISTIC_UUID = 0xFE51;
//
// Tuya identifiers, only to detect and warn users as they are incompatible.
static const uint16_t AM43_TUYA_SERVICE_UUID = 0x1910;
static const uint16_t AM43_TUYA_CHARACTERISTIC_UUID = 0x2b11;
struct Am43Packet {
uint8_t length;
uint8_t data[24];
};
static const uint8_t CMD_GET_BATTERY_LEVEL = 0xA2;
static const uint8_t CMD_GET_LIGHT_LEVEL = 0xAA;
static const uint8_t CMD_GET_POSITION = 0xA7;
static const uint8_t CMD_SEND_PIN = 0x17;
static const uint8_t CMD_SET_STATE = 0x0A;
static const uint8_t CMD_SET_POSITION = 0x0D;
static const uint8_t CMD_NOTIFY_POSITION = 0xA1;
static const uint8_t RESPONSE_ACK = 0x5A;
static const uint8_t RESPONSE_NACK = 0xA5;
class Am43Encoder {
public:
Am43Packet *get_battery_level_request();
Am43Packet *get_light_level_request();
Am43Packet *get_position_request();
Am43Packet *get_send_pin_request(uint16_t pin);
Am43Packet *get_open_request();
Am43Packet *get_close_request();
Am43Packet *get_stop_request();
Am43Packet *get_set_position_request(uint8_t position);
protected:
void checksum_();
Am43Packet *encode_(uint8_t command, uint8_t *data, uint8_t length);
Am43Packet packet_;
};
class Am43Decoder {
public:
void decode(const uint8_t *data, uint16_t length);
bool has_battery_level() { return this->has_battery_level_; }
bool has_light_level() { return this->has_light_level_; }
bool has_set_position_response() { return this->has_set_position_response_; }
bool has_set_state_response() { return this->has_set_state_response_; }
bool has_position() { return this->has_position_; }
bool has_pin_response() { return this->has_pin_response_; }
union {
uint8_t position_;
uint8_t battery_level_;
float light_level_;
uint8_t set_position_ok_;
uint8_t set_state_ok_;
uint8_t pin_ok_;
};
protected:
bool has_battery_level_;
bool has_light_level_;
bool has_set_position_response_;
bool has_set_state_response_;
bool has_position_;
bool has_pin_response_;
};
} // namespace am43
} // namespace esphome

View File

@ -0,0 +1,36 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import cover, ble_client
from esphome.const import CONF_ID, CONF_PIN
CODEOWNERS = ["@buxtronix"]
DEPENDENCIES = ["ble_client"]
AUTO_LOAD = ["am43"]
CONF_INVERT_POSITION = "invert_position"
am43_ns = cg.esphome_ns.namespace("am43")
Am43Component = am43_ns.class_(
"Am43Component", cover.Cover, ble_client.BLEClientNode, cg.Component
)
CONFIG_SCHEMA = (
cover.COVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(Am43Component),
cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF),
cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
}
)
.extend(ble_client.BLE_CLIENT_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_pin(config[CONF_PIN]))
cg.add(var.set_invert_position(config[CONF_INVERT_POSITION]))
yield cg.register_component(var, config)
yield cover.register_cover(var, config)
yield ble_client.register_ble_node(var, config)

View File

@ -0,0 +1,149 @@
#include "am43_cover.h"
#include "esphome/core/log.h"
#ifdef ARDUINO_ARCH_ESP32
namespace esphome {
namespace am43 {
static const char *TAG = "am43_cover";
using namespace esphome::cover;
void Am43Component::dump_config() {
LOG_COVER("", "AM43 Cover", this);
ESP_LOGCONFIG(TAG, " Device Pin: %d", this->pin_);
ESP_LOGCONFIG(TAG, " Invert Position: %d", (int) this->invert_position_);
}
void Am43Component::setup() {
this->position = COVER_OPEN;
this->encoder_ = new Am43Encoder();
this->decoder_ = new Am43Decoder();
this->logged_in_ = false;
}
void Am43Component::loop() {
if (this->node_state == espbt::ClientState::Established && !this->logged_in_) {
auto packet = this->encoder_->get_send_pin_request(this->pin_);
auto status =
esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length,
packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
ESP_LOGI(TAG, "[%s] Logging into AM43", this->get_name().c_str());
if (status)
ESP_LOGW(TAG, "[%s] Error writing set_pin to device, error = %d", this->get_name().c_str(), status);
else
this->logged_in_ = true;
}
}
CoverTraits Am43Component::get_traits() {
auto traits = CoverTraits();
traits.set_supports_position(true);
traits.set_supports_tilt(false);
traits.set_is_assumed_state(false);
return traits;
}
void Am43Component::control(const CoverCall &call) {
if (this->node_state != espbt::ClientState::Established) {
ESP_LOGW(TAG, "[%s] Cannot send cover control, not connected", this->get_name().c_str());
return;
}
if (call.get_stop()) {
auto packet = this->encoder_->get_stop_request();
auto status =
esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length,
packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status)
ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status);
}
if (call.get_position().has_value()) {
auto pos = *call.get_position();
if (this->invert_position_)
pos = 1 - pos;
auto packet = this->encoder_->get_set_position_request(100 - (uint8_t)(pos * 100));
auto status =
esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length,
packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status)
ESP_LOGW(TAG, "[%s] Error writing set_position command to device, error = %d", this->get_name().c_str(), status);
}
}
void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_DISCONNECT_EVT: {
this->logged_in_ = false;
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID);
if (chr == nullptr) {
if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) {
ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->get_name().c_str());
} else {
ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->get_name().c_str());
}
break;
}
this->char_handle_ = chr->handle;
auto status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, chr->handle);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
}
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
this->node_state = espbt::ClientState::Established;
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.handle != this->char_handle_)
break;
this->decoder_->decode(param->notify.value, param->notify.value_len);
if (this->decoder_->has_position()) {
this->position = ((float) this->decoder_->position_ / 100.0);
if (!this->invert_position_)
this->position = 1 - this->position;
if (this->position > 0.97)
this->position = 1.0;
if (this->position < 0.02)
this->position = 0.0;
this->publish_state();
}
if (this->decoder_->has_pin_response()) {
if (this->decoder_->pin_ok_) {
ESP_LOGI(TAG, "[%s] AM43 pin accepted.", this->get_name().c_str());
auto packet = this->encoder_->get_position_request();
auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_,
packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP,
ESP_GATT_AUTH_REQ_NONE);
if (status)
ESP_LOGW(TAG, "[%s] Error writing set_position to device, error = %d", this->get_name().c_str(), status);
} else {
ESP_LOGW(TAG, "[%s] AM43 pin rejected!", this->get_name().c_str());
}
}
if (this->decoder_->has_set_position_response() && !this->decoder_->set_position_ok_)
ESP_LOGW(TAG, "[%s] Got nack after sending set_position. Bad pin?", this->get_name().c_str());
if (this->decoder_->has_set_state_response() && !this->decoder_->set_state_ok_)
ESP_LOGW(TAG, "[%s] Got nack after sending set_state. Bad pin?", this->get_name().c_str());
break;
}
default:
break;
}
}
} // namespace am43
} // namespace esphome
#endif

View File

@ -0,0 +1,45 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/cover/cover.h"
#include "esphome/components/am43/am43_base.h"
#ifdef ARDUINO_ARCH_ESP32
#include <esp_gattc_api.h>
namespace esphome {
namespace am43 {
namespace espbt = esphome::esp32_ble_tracker;
class Am43Component : public cover::Cover, public esphome::ble_client::BLEClientNode, public Component {
public:
void setup() override;
void loop() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
cover::CoverTraits get_traits() override;
void set_pin(uint16_t pin) { this->pin_ = pin; }
void set_invert_position(bool invert_position) { this->invert_position_ = invert_position; }
protected:
void control(const cover::CoverCall &call) override;
uint16_t char_handle_;
uint16_t pin_;
bool invert_position_;
Am43Encoder *encoder_;
Am43Decoder *decoder_;
bool logged_in_;
float position_;
};
} // namespace am43
} // namespace esphome
#endif

View File

@ -0,0 +1,46 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, ble_client
from esphome.const import (
CONF_ID,
CONF_BATTERY_LEVEL,
ICON_BATTERY,
CONF_ILLUMINANCE,
ICON_BRIGHTNESS_5,
UNIT_PERCENT,
)
CODEOWNERS = ["@buxtronix"]
am43_ns = cg.esphome_ns.namespace("am43")
Am43 = am43_ns.class_("Am43", ble_client.BLEClientNode, cg.PollingComponent)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(Am43),
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
UNIT_PERCENT, ICON_BATTERY, 0
),
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
UNIT_PERCENT, ICON_BRIGHTNESS_5, 0
),
}
)
.extend(ble_client.BLE_CLIENT_SCHEMA)
.extend(cv.polling_component_schema("120s"))
)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield ble_client.register_ble_node(var, config)
if CONF_BATTERY_LEVEL in config:
sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
cg.add(var.set_battery(sens))
if CONF_ILLUMINANCE in config:
sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE])
cg.add(var.set_illuminance(sens))

View File

@ -25,7 +25,7 @@ class BLEClientNode {
public:
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) = 0;
virtual void loop() = 0;
virtual void loop(){};
void set_address(uint64_t address) { address_ = address; }
espbt::ESPBTClient *client;
// This should be transitioned to Established once the node no longer needs

View File

@ -2145,6 +2145,10 @@ cover:
id: template_cover
state: CLOSED
assumed_state: no
- platform: am43
name: 'Test AM43'
id: am43_test
ble_client_id: ble_foo
debug: