Merge branch 'dev' into pr/tomaszduda23/7049

This commit is contained in:
Jesse Hills 2024-07-23 10:52:12 +12:00
commit 2c48466243
No known key found for this signature in database
GPG key ID: BEAAE804EFD8E83A
63 changed files with 1144 additions and 199 deletions

View file

@ -37,6 +37,7 @@ esphome/components/am43/sensor/* @buxtronix
esphome/components/analog_threshold/* @ianchi esphome/components/analog_threshold/* @ianchi
esphome/components/animation/* @syndlex esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix esphome/components/anova/* @buxtronix
esphome/components/apds9306/* @aodrenah
esphome/components/api/* @OttoWinter esphome/components/api/* @OttoWinter
esphome/components/as5600/* @ammmze esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze esphome/components/as5600/sensor/* @ammmze
@ -216,6 +217,7 @@ esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core esphome/components/logger/* @esphome/core
esphome/components/ltr390/* @latonita @sjtrny esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr_als_ps/* @latonita esphome/components/ltr_als_ps/* @latonita
esphome/components/m5stack_8angle/* @rnauber
esphome/components/matrix_keypad/* @ssieb esphome/components/matrix_keypad/* @ssieb
esphome/components/max31865/* @DAVe3283 esphome/components/max31865/* @DAVe3283
esphome/components/max44009/* @berfenger esphome/components/max44009/* @berfenger

View file

@ -1,5 +1,5 @@
import esphome.config_validation as cv import esphome.config_validation as cv
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( CONFIG_SCHEMA = cv.invalid(
"The ade7953 sensor component has been renamed to ade7953_i2c." "The ade7953 sensor component has been renamed to ade7953_i2c."
) )

View file

@ -0,0 +1,4 @@
# Based on this datasheet:
# https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
CODEOWNERS = ["@aodrenah"]

View file

@ -0,0 +1,151 @@
// Based on this datasheet:
// https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
#include "apds9306.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace apds9306 {
static const char *const TAG = "apds9306";
enum { // APDS9306 registers
APDS9306_MAIN_CTRL = 0x00,
APDS9306_ALS_MEAS_RATE = 0x04,
APDS9306_ALS_GAIN = 0x05,
APDS9306_PART_ID = 0x06,
APDS9306_MAIN_STATUS = 0x07,
APDS9306_CLEAR_DATA_0 = 0x0A, // LSB
APDS9306_CLEAR_DATA_1 = 0x0B,
APDS9306_CLEAR_DATA_2 = 0x0C, // MSB
APDS9306_ALS_DATA_0 = 0x0D, // LSB
APDS9306_ALS_DATA_1 = 0x0E,
APDS9306_ALS_DATA_2 = 0x0F, // MSB
APDS9306_INT_CFG = 0x19,
APDS9306_INT_PERSISTENCE = 0x1A,
APDS9306_ALS_THRES_UP_0 = 0x21, // LSB
APDS9306_ALS_THRES_UP_1 = 0x22,
APDS9306_ALS_THRES_UP_2 = 0x23, // MSB
APDS9306_ALS_THRES_LOW_0 = 0x24, // LSB
APDS9306_ALS_THRES_LOW_1 = 0x25,
APDS9306_ALS_THRES_LOW_2 = 0x26, // MSB
APDS9306_ALS_THRES_VAR = 0x27
};
#define APDS9306_ERROR_CHECK(func, error) \
if (!(func)) { \
ESP_LOGE(TAG, error); \
this->mark_failed(); \
return; \
}
#define APDS9306_WARNING_CHECK(func, warning) \
if (!(func)) { \
ESP_LOGW(TAG, warning); \
this->status_set_warning(); \
return; \
}
#define APDS9306_WRITE_BYTE(reg, value) \
ESP_LOGV(TAG, "Writing 0x%02x to 0x%02x", value, reg); \
if (!this->write_byte(reg, value)) { \
ESP_LOGE(TAG, "Failed writing 0x%02x to 0x%02x", value, reg); \
this->mark_failed(); \
return; \
}
void APDS9306::setup() {
ESP_LOGCONFIG(TAG, "Setting up APDS9306...");
uint8_t id;
if (!this->read_byte(APDS9306_PART_ID, &id)) { // Part ID register
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
if (id != 0xB1 && id != 0xB3) { // 0xB1 for APDS9306 0xB3 for APDS9306-065
this->error_code_ = WRONG_ID;
this->mark_failed();
return;
}
// ALS resolution and measurement, see datasheet or init.py for options
uint8_t als_meas_rate = ((this->bit_width_ & 0x07) << 4) | (this->measurement_rate_ & 0x07);
APDS9306_WRITE_BYTE(APDS9306_ALS_MEAS_RATE, als_meas_rate);
// ALS gain, see datasheet or init.py for options
uint8_t als_gain = (this->gain_ & 0x07);
APDS9306_WRITE_BYTE(APDS9306_ALS_GAIN, als_gain);
// Set to standby mode
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x00);
// Check for data, clear main status
uint8_t status;
APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed.");
// Set to active mode
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02);
ESP_LOGCONFIG(TAG, "APDS9306 setup complete");
}
void APDS9306::dump_config() {
LOG_SENSOR("", "APDS9306", this);
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGE(TAG, "Communication with APDS9306 failed!");
break;
case WRONG_ID:
ESP_LOGE(TAG, "APDS9306 has invalid id!");
break;
default:
ESP_LOGE(TAG, "Setting up APDS9306 registers failed!");
break;
}
}
ESP_LOGCONFIG(TAG, " Gain: %u", AMBIENT_LIGHT_GAIN_VALUES[this->gain_]);
ESP_LOGCONFIG(TAG, " Measurement rate: %u", MEASUREMENT_RATE_VALUES[this->measurement_rate_]);
ESP_LOGCONFIG(TAG, " Measurement Resolution/Bit width: %d", MEASUREMENT_BIT_WIDTH_VALUES[this->bit_width_]);
LOG_UPDATE_INTERVAL(this);
}
void APDS9306::update() {
// Check for new data
uint8_t status;
APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed.");
this->status_clear_warning();
if (!(status &= 0b00001000)) { // No new data
return;
}
// Set to standby mode
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x00);
// Clear MAIN STATUS
APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed.");
uint8_t als_data[3];
APDS9306_WARNING_CHECK(this->read_bytes(APDS9306_ALS_DATA_0, als_data, 3), "Reading ALS data has failed.");
// Set to active mode
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02);
uint32_t light_level = 0x00 | encode_uint24(als_data[2], als_data[1], als_data[0]);
float lux = ((float) light_level / AMBIENT_LIGHT_GAIN_VALUES[this->gain_]) *
(100.0f / MEASUREMENT_RATE_VALUES[this->measurement_rate_]);
ESP_LOGD(TAG, "Got illuminance=%.1flx from", lux);
this->publish_state(lux);
}
} // namespace apds9306
} // namespace esphome

View file

@ -0,0 +1,66 @@
// Based on this datasheet:
// https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace apds9306 {
enum MeasurementBitWidth : uint8_t {
MEASUREMENT_BIT_WIDTH_20 = 0,
MEASUREMENT_BIT_WIDTH_19 = 1,
MEASUREMENT_BIT_WIDTH_18 = 2,
MEASUREMENT_BIT_WIDTH_17 = 3,
MEASUREMENT_BIT_WIDTH_16 = 4,
MEASUREMENT_BIT_WIDTH_13 = 5,
};
static const uint8_t MEASUREMENT_BIT_WIDTH_VALUES[] = {20, 19, 18, 17, 16, 13};
enum MeasurementRate : uint8_t {
MEASUREMENT_RATE_25 = 0,
MEASUREMENT_RATE_50 = 1,
MEASUREMENT_RATE_100 = 2,
MEASUREMENT_RATE_200 = 3,
MEASUREMENT_RATE_500 = 4,
MEASUREMENT_RATE_1000 = 5,
MEASUREMENT_RATE_2000 = 6,
};
static const uint16_t MEASUREMENT_RATE_VALUES[] = {25, 50, 100, 200, 500, 1000, 2000};
enum AmbientLightGain : uint8_t {
AMBIENT_LIGHT_GAIN_1 = 0,
AMBIENT_LIGHT_GAIN_3 = 1,
AMBIENT_LIGHT_GAIN_6 = 2,
AMBIENT_LIGHT_GAIN_9 = 3,
AMBIENT_LIGHT_GAIN_18 = 4,
};
static const uint8_t AMBIENT_LIGHT_GAIN_VALUES[] = {1, 3, 6, 9, 18};
class APDS9306 : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::BUS; }
void dump_config() override;
void update() override;
void set_bit_width(MeasurementBitWidth bit_width) { this->bit_width_ = bit_width; }
void set_measurement_rate(MeasurementRate measurement_rate) { this->measurement_rate_ = measurement_rate; }
void set_ambient_light_gain(AmbientLightGain gain) { this->gain_ = gain; }
protected:
enum ErrorCode {
NONE = 0,
COMMUNICATION_FAILED,
WRONG_ID,
} error_code_{NONE};
MeasurementBitWidth bit_width_;
MeasurementRate measurement_rate_;
AmbientLightGain gain_;
};
} // namespace apds9306
} // namespace esphome

View file

@ -0,0 +1,95 @@
# Based on this datasheet:
# https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_GAIN,
DEVICE_CLASS_ILLUMINANCE,
ICON_LIGHTBULB,
STATE_CLASS_MEASUREMENT,
UNIT_LUX,
)
DEPENDENCIES = ["i2c"]
CONF_APDS9306_ID = "apds9306_id"
CONF_BIT_WIDTH = "bit_width"
CONF_MEASUREMENT_RATE = "measurement_rate"
apds9306_ns = cg.esphome_ns.namespace("apds9306")
APDS9306 = apds9306_ns.class_(
"APDS9306", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
)
MeasurementBitWidth = apds9306_ns.enum("MeasurementBitWidth")
MeasurementRate = apds9306_ns.enum("MeasurementRate")
AmbientLightGain = apds9306_ns.enum("AmbientLightGain")
MEASUREMENT_BIT_WIDTHS = {
20: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_20,
19: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_19,
18: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_18,
17: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_17,
16: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_16,
13: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_13,
}
MEASUREMENT_RATES = {
25: MeasurementRate.MEASUREMENT_RATE_25,
50: MeasurementRate.MEASUREMENT_RATE_50,
100: MeasurementRate.MEASUREMENT_RATE_100,
200: MeasurementRate.MEASUREMENT_RATE_200,
500: MeasurementRate.MEASUREMENT_RATE_500,
1000: MeasurementRate.MEASUREMENT_RATE_1000,
2000: MeasurementRate.MEASUREMENT_RATE_2000,
}
AMBIENT_LIGHT_GAINS = {
1: AmbientLightGain.AMBIENT_LIGHT_GAIN_1,
3: AmbientLightGain.AMBIENT_LIGHT_GAIN_3,
6: AmbientLightGain.AMBIENT_LIGHT_GAIN_6,
9: AmbientLightGain.AMBIENT_LIGHT_GAIN_9,
18: AmbientLightGain.AMBIENT_LIGHT_GAIN_18,
}
def _validate_measurement_rate(value):
value = cv.positive_time_period_milliseconds(value)
return cv.enum(MEASUREMENT_RATES, int=True)(value.total_milliseconds)
CONFIG_SCHEMA = (
sensor.sensor_schema(
APDS9306,
unit_of_measurement=UNIT_LUX,
accuracy_decimals=1,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICON_LIGHTBULB,
)
.extend(
{
cv.Optional(CONF_GAIN, default="1"): cv.enum(AMBIENT_LIGHT_GAINS, int=True),
cv.Optional(CONF_BIT_WIDTH, default="18"): cv.enum(
MEASUREMENT_BIT_WIDTHS, int=True
),
cv.Optional(
CONF_MEASUREMENT_RATE, default="100ms"
): _validate_measurement_rate,
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x52))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_bit_width(config[CONF_BIT_WIDTH]))
cg.add(var.set_measurement_rate(config[CONF_MEASUREMENT_RATE]))
cg.add(var.set_ambient_light_gain(config[CONF_GAIN]))

View file

@ -2,6 +2,6 @@ import esphome.config_validation as cv
CODEOWNERS = ["@latonita"] CODEOWNERS = ["@latonita"]
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( CONFIG_SCHEMA = cv.invalid(
"The bmp3xx sensor component has been renamed to bmp3xx_i2c." "The bmp3xx sensor component has been renamed to bmp3xx_i2c."
) )

View file

@ -2,6 +2,6 @@ import esphome.config_validation as cv
CODEOWNERS = ["@latonita"] CODEOWNERS = ["@latonita"]
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( CONFIG_SCHEMA = cv.invalid(
"The ens160 sensor component has been renamed to ens160_i2c." "The ens160 sensor component has been renamed to ens160_i2c."
) )

View file

@ -7,10 +7,10 @@ from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@jesserockz", "@Rapsssito"] CODEOWNERS = ["@jesserockz", "@Rapsssito"]
CONFLICTS_WITH = ["esp32_ble_beacon"]
CONF_BLE_ID = "ble_id" CONF_BLE_ID = "ble_id"
CONF_IO_CAPABILITY = "io_capability" CONF_IO_CAPABILITY = "io_capability"
CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time"
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
@ -34,6 +34,19 @@ IO_CAPABILITY = {
"display_yes_no": IoCapability.IO_CAP_IO, "display_yes_no": IoCapability.IO_CAP_IO,
} }
esp_power_level_t = cg.global_ns.enum("esp_power_level_t")
TX_POWER_LEVELS = {
-12: esp_power_level_t.ESP_PWR_LVL_N12,
-9: esp_power_level_t.ESP_PWR_LVL_N9,
-6: esp_power_level_t.ESP_PWR_LVL_N6,
-3: esp_power_level_t.ESP_PWR_LVL_N3,
0: esp_power_level_t.ESP_PWR_LVL_N0,
3: esp_power_level_t.ESP_PWR_LVL_P3,
6: esp_power_level_t.ESP_PWR_LVL_P6,
9: esp_power_level_t.ESP_PWR_LVL_P9,
}
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(ESP32BLE), cv.GenerateID(): cv.declare_id(ESP32BLE),
@ -41,6 +54,9 @@ CONFIG_SCHEMA = cv.Schema(
IO_CAPABILITY, lower=True IO_CAPABILITY, lower=True
), ),
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(
CONF_ADVERTISING_CYCLE_TIME, default="10s"
): cv.positive_time_period_milliseconds,
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
@ -58,6 +74,7 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY]))
cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME]))
await cg.register_component(var, config) await cg.register_component(var, config)
if CORE.using_esp_idf: if CORE.using_esp_idf:

View file

@ -78,6 +78,11 @@ void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &dat
this->advertising_start(); this->advertising_start();
} }
void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback) {
this->advertising_init_();
this->advertising_->register_raw_advertisement_callback(std::move(callback));
}
void ESP32BLE::advertising_add_service_uuid(ESPBTUUID uuid) { void ESP32BLE::advertising_add_service_uuid(ESPBTUUID uuid) {
this->advertising_init_(); this->advertising_init_();
this->advertising_->add_service_uuid(uuid); this->advertising_->add_service_uuid(uuid);
@ -102,7 +107,7 @@ bool ESP32BLE::ble_pre_setup_() {
void ESP32BLE::advertising_init_() { void ESP32BLE::advertising_init_() {
if (this->advertising_ != nullptr) if (this->advertising_ != nullptr)
return; return;
this->advertising_ = new BLEAdvertising(); // NOLINT(cppcoreguidelines-owning-memory) this->advertising_ = new BLEAdvertising(this->advertising_cycle_time_); // NOLINT(cppcoreguidelines-owning-memory)
this->advertising_->set_scan_response(true); this->advertising_->set_scan_response(true);
this->advertising_->set_min_preferred_interval(0x06); this->advertising_->set_min_preferred_interval(0x06);
@ -312,6 +317,9 @@ void ESP32BLE::loop() {
delete ble_event; // NOLINT(cppcoreguidelines-owning-memory) delete ble_event; // NOLINT(cppcoreguidelines-owning-memory)
ble_event = this->ble_events_.pop(); ble_event = this->ble_events_.pop();
} }
if (this->advertising_ != nullptr) {
this->advertising_->loop();
}
} }
void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {

View file

@ -3,6 +3,8 @@
#include "ble_advertising.h" #include "ble_advertising.h"
#include "ble_uuid.h" #include "ble_uuid.h"
#include <functional>
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
@ -76,6 +78,11 @@ class ESP32BLE : public Component {
public: public:
void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; }
void set_advertising_cycle_time(uint32_t advertising_cycle_time) {
this->advertising_cycle_time_ = advertising_cycle_time;
}
uint32_t get_advertising_cycle_time() const { return this->advertising_cycle_time_; }
void enable(); void enable();
void disable(); void disable();
bool is_active(); bool is_active();
@ -89,6 +96,7 @@ class ESP32BLE : public Component {
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data); void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_add_service_uuid(ESPBTUUID uuid); void advertising_add_service_uuid(ESPBTUUID uuid);
void advertising_remove_service_uuid(ESPBTUUID uuid); void advertising_remove_service_uuid(ESPBTUUID uuid);
void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback);
void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); }
void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); }
@ -121,6 +129,7 @@ class ESP32BLE : public Component {
Queue<BLEEvent> ble_events_; Queue<BLEEvent> ble_events_;
BLEAdvertising *advertising_; BLEAdvertising *advertising_;
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
uint32_t advertising_cycle_time_;
bool enable_on_boot_; bool enable_on_boot_;
}; };

View file

@ -10,9 +10,9 @@
namespace esphome { namespace esphome {
namespace esp32_ble { namespace esp32_ble {
static const char *const TAG = "esp32_ble"; static const char *const TAG = "esp32_ble.advertising";
BLEAdvertising::BLEAdvertising() { BLEAdvertising::BLEAdvertising(uint32_t advertising_cycle_time) : advertising_cycle_time_(advertising_cycle_time) {
this->advertising_data_.set_scan_rsp = false; this->advertising_data_.set_scan_rsp = false;
this->advertising_data_.include_name = true; this->advertising_data_.include_name = true;
this->advertising_data_.include_txpower = true; this->advertising_data_.include_txpower = true;
@ -64,7 +64,7 @@ void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
} }
} }
void BLEAdvertising::start() { esp_err_t BLEAdvertising::services_advertisement_() {
int num_services = this->advertising_uuids_.size(); int num_services = this->advertising_uuids_.size();
if (num_services == 0) { if (num_services == 0) {
this->advertising_data_.service_uuid_len = 0; this->advertising_data_.service_uuid_len = 0;
@ -87,8 +87,8 @@ void BLEAdvertising::start() {
this->advertising_data_.include_txpower = !this->scan_response_; this->advertising_data_.include_txpower = !this->scan_response_;
err = esp_ble_gap_config_adv_data(&this->advertising_data_); err = esp_ble_gap_config_adv_data(&this->advertising_data_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Advertising): %d", err); ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Advertising): %s", esp_err_to_name(err));
return; return err;
} }
if (this->scan_response_) { if (this->scan_response_) {
@ -101,8 +101,8 @@ void BLEAdvertising::start() {
this->scan_response_data_.flag = 0; this->scan_response_data_.flag = 0;
err = esp_ble_gap_config_adv_data(&this->scan_response_data_); err = esp_ble_gap_config_adv_data(&this->scan_response_data_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Scan response): %d", err); ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Scan response): %s", esp_err_to_name(err));
return; return err;
} }
} }
@ -113,8 +113,18 @@ void BLEAdvertising::start() {
err = esp_ble_gap_start_advertising(&this->advertising_params_); err = esp_ble_gap_start_advertising(&this->advertising_params_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %d", err); ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %s", esp_err_to_name(err));
return; return err;
}
return ESP_OK;
}
void BLEAdvertising::start() {
if (this->current_adv_index_ == -1) {
this->services_advertisement_();
} else {
this->raw_advertisements_callbacks_[this->current_adv_index_](true);
} }
} }
@ -124,6 +134,29 @@ void BLEAdvertising::stop() {
ESP_LOGE(TAG, "esp_ble_gap_stop_advertising failed: %d", err); ESP_LOGE(TAG, "esp_ble_gap_stop_advertising failed: %d", err);
return; return;
} }
if (this->current_adv_index_ != -1) {
this->raw_advertisements_callbacks_[this->current_adv_index_](false);
}
}
void BLEAdvertising::loop() {
if (this->raw_advertisements_callbacks_.empty()) {
return;
}
const uint32_t now = millis();
if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) {
this->stop();
this->current_adv_index_ += 1;
if (this->current_adv_index_ >= this->raw_advertisements_callbacks_.size()) {
this->current_adv_index_ = -1;
}
this->start();
this->last_advertisement_time_ = now;
}
}
void BLEAdvertising::register_raw_advertisement_callback(std::function<void(bool)> &&callback) {
this->raw_advertisements_callbacks_.push_back(std::move(callback));
} }
} // namespace esp32_ble } // namespace esp32_ble

View file

@ -1,20 +1,31 @@
#pragma once #pragma once
#include <array>
#include <functional>
#include <vector> #include <vector>
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_bt.h>
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
#include <esp_gatts_api.h> #include <esp_gatts_api.h>
namespace esphome { namespace esphome {
namespace esp32_ble { namespace esp32_ble {
using raw_adv_data_t = struct {
uint8_t *data;
size_t length;
esp_power_level_t power_level;
};
class ESPBTUUID; class ESPBTUUID;
class BLEAdvertising { class BLEAdvertising {
public: public:
BLEAdvertising(); BLEAdvertising(uint32_t advertising_cycle_time);
void loop();
void add_service_uuid(ESPBTUUID uuid); void add_service_uuid(ESPBTUUID uuid);
void remove_service_uuid(ESPBTUUID uuid); void remove_service_uuid(ESPBTUUID uuid);
@ -22,16 +33,25 @@ class BLEAdvertising {
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data); void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_service_data(const std::vector<uint8_t> &data); void set_service_data(const std::vector<uint8_t> &data);
void register_raw_advertisement_callback(std::function<void(bool)> &&callback);
void start(); void start();
void stop(); void stop();
protected: protected:
esp_err_t services_advertisement_();
bool scan_response_; bool scan_response_;
esp_ble_adv_data_t advertising_data_; esp_ble_adv_data_t advertising_data_;
esp_ble_adv_data_t scan_response_data_; esp_ble_adv_data_t scan_response_data_;
esp_ble_adv_params_t advertising_params_; esp_ble_adv_params_t advertising_params_;
std::vector<ESPBTUUID> advertising_uuids_; std::vector<ESPBTUUID> advertising_uuids_;
std::vector<std::function<void(bool)>> raw_advertisements_callbacks_;
const uint32_t advertising_cycle_time_;
uint32_t last_advertisement_time_{0};
int8_t current_adv_index_{-1}; // -1 means standard scan response
}; };
} // namespace esp32_ble } // namespace esp32_ble

View file

@ -1,16 +1,21 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components.esp32_ble import CONF_BLE_ID
from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID, CONF_TX_POWER from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID, CONF_TX_POWER
from esphome.core import CORE, TimePeriod from esphome.core import CORE, TimePeriod
from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components import esp32_ble from esphome.components import esp32_ble
AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
CONFLICTS_WITH = ["esp32_ble_tracker"]
esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon") esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon")
ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component) ESP32BLEBeacon = esp32_ble_beacon_ns.class_(
"ESP32BLEBeacon",
cg.Component,
esp32_ble.GAPEventHandler,
cg.Parented.template(esp32_ble.ESP32BLE),
)
CONF_MAJOR = "major" CONF_MAJOR = "major"
CONF_MINOR = "minor" CONF_MINOR = "minor"
CONF_MIN_INTERVAL = "min_interval" CONF_MIN_INTERVAL = "min_interval"
@ -28,6 +33,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(ESP32BLEBeacon), cv.GenerateID(): cv.declare_id(ESP32BLEBeacon),
cv.GenerateID(CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True), cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True),
cv.Required(CONF_UUID): cv.uuid, cv.Required(CONF_UUID): cv.uuid,
cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t, cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t,
@ -48,7 +54,7 @@ CONFIG_SCHEMA = cv.All(
min=-128, max=0 min=-128, max=0
), ),
cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All( cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All(
cv.decibel, cv.one_of(-12, -9, -6, -3, 0, 3, 6, 9, int=True) cv.decibel, cv.enum(esp32_ble.TX_POWER_LEVELS, int=True)
), ),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
@ -62,6 +68,10 @@ async def to_code(config):
uuid = config[CONF_UUID].hex uuid = config[CONF_UUID].hex
uuid_arr = [cg.RawExpression(f"0x{uuid[i:i + 2]}") for i in range(0, len(uuid), 2)] uuid_arr = [cg.RawExpression(f"0x{uuid[i:i + 2]}") for i in range(0, len(uuid), 2)]
var = cg.new_Pvariable(config[CONF_ID], uuid_arr) var = cg.new_Pvariable(config[CONF_ID], uuid_arr)
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
cg.add(parent.register_gap_event_handler(var))
await cg.register_component(var, config) await cg.register_component(var, config)
cg.add(var.set_major(config[CONF_MAJOR])) cg.add(var.set_major(config[CONF_MAJOR]))
cg.add(var.set_minor(config[CONF_MINOR])) cg.add(var.set_minor(config[CONF_MINOR]))

View file

@ -3,14 +3,16 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <nvs_flash.h>
#include <freertos/FreeRTOS.h>
#include <esp_bt_main.h>
#include <esp_bt.h> #include <esp_bt.h>
#include <freertos/task.h> #include <esp_bt_main.h>
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#include <cstring> #include <cstring>
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#include <esp32-hal-bt.h> #include <esp32-hal-bt.h>
@ -21,20 +23,6 @@ namespace esp32_ble_beacon {
static const char *const TAG = "esp32_ble_beacon"; static const char *const TAG = "esp32_ble_beacon";
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static esp_ble_adv_params_t ble_adv_params = {
.adv_int_min = 0x20,
.adv_int_max = 0x40,
.adv_type = ADV_TYPE_NONCONN_IND,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.peer_addr = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.peer_addr_type = BLE_ADDR_TYPE_PUBLIC,
.channel_map = ADV_CHNL_ALL,
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};
#define ENDIAN_CHANGE_U16(x) ((((x) &0xFF00) >> 8) + (((x) &0xFF) << 8))
static const esp_ble_ibeacon_head_t IBEACON_COMMON_HEAD = { static const esp_ble_ibeacon_head_t IBEACON_COMMON_HEAD = {
.flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = {0x4C, 0x00}, .beacon_type = {0x02, 0x15}}; .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = {0x4C, 0x00}, .beacon_type = {0x02, 0x15}};
@ -53,117 +41,62 @@ void ESP32BLEBeacon::dump_config() {
" UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d" " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d"
", TX Power: %ddBm", ", TX Power: %ddBm",
uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_, uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_,
this->tx_power_); (this->tx_power_ * 3) - 12);
} }
float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void ESP32BLEBeacon::setup() { void ESP32BLEBeacon::setup() {
ESP_LOGCONFIG(TAG, "Setting up ESP32 BLE beacon..."); this->ble_adv_params_ = {
global_esp32_ble_beacon = this; .adv_int_min = static_cast<uint16_t>(this->min_interval_ / 0.625f),
.adv_int_max = static_cast<uint16_t>(this->max_interval_ / 0.625f),
.adv_type = ADV_TYPE_NONCONN_IND,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.peer_addr = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.peer_addr_type = BLE_ADDR_TYPE_PUBLIC,
.channel_map = ADV_CHNL_ALL,
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};
xTaskCreatePinnedToCore(ESP32BLEBeacon::ble_core_task, global_ble->advertising_register_raw_advertisement_callback([this](bool advertise) {
"ble_task", // name this->advertising_ = advertise;
10000, // stack size (in words) if (advertise) {
nullptr, // input params this->on_advertise_();
1, // priority }
nullptr, // Handle, not needed });
0 // core
);
} }
float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::BLUETOOTH; } void ESP32BLEBeacon::on_advertise_() {
void ESP32BLEBeacon::ble_core_task(void *params) {
ble_setup();
while (true) {
delay(1000); // NOLINT
}
}
void ESP32BLEBeacon::ble_setup() {
ble_adv_params.adv_int_min = static_cast<uint16_t>(global_esp32_ble_beacon->min_interval_ / 0.625f);
ble_adv_params.adv_int_max = static_cast<uint16_t>(global_esp32_ble_beacon->max_interval_ / 0.625f);
// Initialize non-volatile storage for the bluetooth controller
esp_err_t err = nvs_flash_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_flash_init failed: %d", err);
return;
}
#ifdef USE_ARDUINO
if (!btStart()) {
ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status());
return;
}
#else
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
// start bt controller
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) {
esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
err = esp_bt_controller_init(&cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err));
return;
}
while (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE)
;
}
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED) {
err = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bt_controller_enable failed: %s", esp_err_to_name(err));
return;
}
}
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
ESP_LOGE(TAG, "esp bt controller enable failed");
return;
}
}
#endif
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
err = esp_bluedroid_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err);
return;
}
err = esp_bluedroid_enable();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err);
return;
}
err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV,
static_cast<esp_power_level_t>((global_esp32_ble_beacon->tx_power_ + 12) / 3));
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err));
return;
}
err = esp_ble_gap_register_callback(ESP32BLEBeacon::gap_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
return;
}
esp_ble_ibeacon_t ibeacon_adv_data; esp_ble_ibeacon_t ibeacon_adv_data;
memcpy(&ibeacon_adv_data.ibeacon_head, &IBEACON_COMMON_HEAD, sizeof(esp_ble_ibeacon_head_t)); memcpy(&ibeacon_adv_data.ibeacon_head, &IBEACON_COMMON_HEAD, sizeof(esp_ble_ibeacon_head_t));
memcpy(&ibeacon_adv_data.ibeacon_vendor.proximity_uuid, global_esp32_ble_beacon->uuid_.data(), memcpy(&ibeacon_adv_data.ibeacon_vendor.proximity_uuid, this->uuid_.data(),
sizeof(ibeacon_adv_data.ibeacon_vendor.proximity_uuid)); sizeof(ibeacon_adv_data.ibeacon_vendor.proximity_uuid));
ibeacon_adv_data.ibeacon_vendor.minor = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->minor_); ibeacon_adv_data.ibeacon_vendor.minor = byteswap(this->minor_);
ibeacon_adv_data.ibeacon_vendor.major = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->major_); ibeacon_adv_data.ibeacon_vendor.major = byteswap(this->major_);
ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast<uint8_t>(global_esp32_ble_beacon->measured_power_); ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast<uint8_t>(this->measured_power_);
esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data)); ESP_LOGD(TAG, "Setting BLE TX power");
esp_err_t err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err));
}
err = esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data));
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data_raw failed: %s", esp_err_to_name(err));
return;
}
} }
void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
if (!this->advertising_)
return;
esp_err_t err; esp_err_t err;
switch (event) { switch (event) {
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: { case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: {
err = esp_ble_gap_start_advertising(&ble_adv_params); err = esp_ble_gap_start_advertising(&this->ble_adv_params_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %d", err); ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %s", esp_err_to_name(err));
} }
break; break;
} }
@ -181,6 +114,7 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap
} else { } else {
ESP_LOGD(TAG, "BLE stopped advertising successfully"); ESP_LOGD(TAG, "BLE stopped advertising successfully");
} }
// this->advertising_ = false;
break; break;
} }
default: default:
@ -188,8 +122,6 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap
} }
} }
ESP32BLEBeacon *global_esp32_ble_beacon = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esp32_ble_beacon } // namespace esp32_ble_beacon
} // namespace esphome } // namespace esphome

View file

@ -1,39 +1,39 @@
#pragma once #pragma once
#include "esphome/components/esp32_ble/ble.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_gap_ble_api.h>
#include <esp_bt.h> #include <esp_bt.h>
#include <esp_gap_ble_api.h>
namespace esphome { namespace esphome {
namespace esp32_ble_beacon { namespace esp32_ble_beacon {
// NOLINTNEXTLINE(modernize-use-using) using esp_ble_ibeacon_head_t = struct {
typedef struct {
uint8_t flags[3]; uint8_t flags[3];
uint8_t length; uint8_t length;
uint8_t type; uint8_t type;
uint8_t company_id[2]; uint8_t company_id[2];
uint8_t beacon_type[2]; uint8_t beacon_type[2];
} __attribute__((packed)) esp_ble_ibeacon_head_t; } __attribute__((packed));
// NOLINTNEXTLINE(modernize-use-using) using esp_ble_ibeacon_vendor_t = struct {
typedef struct {
uint8_t proximity_uuid[16]; uint8_t proximity_uuid[16];
uint16_t major; uint16_t major;
uint16_t minor; uint16_t minor;
uint8_t measured_power; uint8_t measured_power;
} __attribute__((packed)) esp_ble_ibeacon_vendor_t; } __attribute__((packed));
// NOLINTNEXTLINE(modernize-use-using) using esp_ble_ibeacon_t = struct {
typedef struct {
esp_ble_ibeacon_head_t ibeacon_head; esp_ble_ibeacon_head_t ibeacon_head;
esp_ble_ibeacon_vendor_t ibeacon_vendor; esp_ble_ibeacon_vendor_t ibeacon_vendor;
} __attribute__((packed)) esp_ble_ibeacon_t; } __attribute__((packed));
class ESP32BLEBeacon : public Component { using namespace esp32_ble;
class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented<ESP32BLE> {
public: public:
explicit ESP32BLEBeacon(const std::array<uint8_t, 16> &uuid) : uuid_(uuid) {} explicit ESP32BLEBeacon(const std::array<uint8_t, 16> &uuid) : uuid_(uuid) {}
@ -46,12 +46,11 @@ class ESP32BLEBeacon : public Component {
void set_min_interval(uint16_t val) { this->min_interval_ = val; } void set_min_interval(uint16_t val) { this->min_interval_ = val; }
void set_max_interval(uint16_t val) { this->max_interval_ = val; } void set_max_interval(uint16_t val) { this->max_interval_ = val; }
void set_measured_power(int8_t val) { this->measured_power_ = val; } void set_measured_power(int8_t val) { this->measured_power_ = val; }
void set_tx_power(int8_t val) { this->tx_power_ = val; } void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; }
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
protected: protected:
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); void on_advertise_();
static void ble_core_task(void *params);
static void ble_setup();
std::array<uint8_t, 16> uuid_; std::array<uint8_t, 16> uuid_;
uint16_t major_{}; uint16_t major_{};
@ -59,12 +58,11 @@ class ESP32BLEBeacon : public Component {
uint16_t min_interval_{}; uint16_t min_interval_{};
uint16_t max_interval_{}; uint16_t max_interval_{};
int8_t measured_power_{}; int8_t measured_power_{};
int8_t tx_power_{}; esp_power_level_t tx_power_{};
esp_ble_adv_params_t ble_adv_params_;
bool advertising_{false};
}; };
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern ESP32BLEBeacon *global_esp32_ble_beacon;
} // namespace esp32_ble_beacon } // namespace esp32_ble_beacon
} // namespace esphome } // namespace esphome

View file

@ -7,7 +7,6 @@ from esphome.components.esp32 import add_idf_sdkconfig_option
AUTO_LOAD = ["esp32_ble"] AUTO_LOAD = ["esp32_ble"]
CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"]
CONFLICTS_WITH = ["esp32_ble_beacon"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
CONF_MANUFACTURER = "manufacturer" CONF_MANUFACTURER = "manufacturer"

View file

@ -6,7 +6,6 @@ from esphome.const import CONF_ID
AUTO_LOAD = ["esp32_ble_server"] AUTO_LOAD = ["esp32_ble_server"]
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
CONFLICTS_WITH = ["esp32_ble_beacon"]
DEPENDENCIES = ["wifi", "esp32"] DEPENDENCIES = ["wifi", "esp32"]
CONF_AUTHORIZED_DURATION = "authorized_duration" CONF_AUTHORIZED_DURATION = "authorized_duration"

View file

@ -8,6 +8,7 @@ from esphome.const import (
CONF_PROTOCOL, CONF_PROTOCOL,
CONF_VISUAL, CONF_VISUAL,
) )
from esphome.core import CORE
CODEOWNERS = ["@rob-deutsch"] CODEOWNERS = ["@rob-deutsch"]
@ -127,3 +128,5 @@ def to_code(config):
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
cg.add_library("tonia/HeatpumpIR", "1.0.27") cg.add_library("tonia/HeatpumpIR", "1.0.27")
if CORE.is_libretiny:
CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")

View file

@ -52,6 +52,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
config.timeout_ms = this->timeout_; config.timeout_ms = this->timeout_;
config.disable_auto_redirect = !this->follow_redirects_; config.disable_auto_redirect = !this->follow_redirects_;
config.max_redirection_count = this->redirect_limit_; config.max_redirection_count = this->redirect_limit_;
config.auth_type = HTTP_AUTH_TYPE_BASIC;
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
if (secure) { if (secure) {
config.crt_bundle_attach = esp_crt_bundle_attach; config.crt_bundle_attach = esp_crt_bundle_attach;

View file

@ -174,7 +174,8 @@ size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) {
size_t samples_read = bytes_read / sizeof(int32_t); size_t samples_read = bytes_read / sizeof(int32_t);
samples.resize(samples_read); samples.resize(samples_read);
for (size_t i = 0; i < samples_read; i++) { for (size_t i = 0; i < samples_read; i++) {
samples[i] = reinterpret_cast<int32_t *>(buf)[i] >> 16; int32_t temp = reinterpret_cast<int32_t *>(buf)[i] >> 14;
samples[i] = clamp<int16_t>(temp, INT16_MIN, INT16_MAX);
} }
memcpy(buf, samples.data(), samples_read * sizeof(int16_t)); memcpy(buf, samples.data(), samples_read * sizeof(int16_t));
return samples_read * sizeof(int16_t); return samples_read * sizeof(int16_t);

View file

@ -92,7 +92,9 @@ static const uint8_t ILI9XXX_GMCTRN1 = 0xE1;
static const uint8_t ILI9XXX_CSCON = 0xF0; static const uint8_t ILI9XXX_CSCON = 0xF0;
static const uint8_t ILI9XXX_ADJCTL3 = 0xF7; static const uint8_t ILI9XXX_ADJCTL3 = 0xF7;
static const uint8_t ILI9XXX_DELAY = 0xFF; // followed by one byte of delay time in ms static const uint8_t ILI9XXX_DELAY_FLAG = 0xFF;
// special marker for delay - command byte reprents ms, length byte is an impossible value
#define ILI9XXX_DELAY(ms) ((uint8_t) ((ms) | 0x80)), ILI9XXX_DELAY_FLAG
} // namespace ili9xxx } // namespace ili9xxx
} // namespace esphome } // namespace esphome

View file

@ -34,8 +34,8 @@ void ILI9XXXDisplay::setup() {
ESP_LOGD(TAG, "Setting up ILI9xxx"); ESP_LOGD(TAG, "Setting up ILI9xxx");
this->setup_pins_(); this->setup_pins_();
this->init_lcd(this->init_sequence_); this->init_lcd_(this->init_sequence_);
this->init_lcd(this->extra_init_sequence_.data()); this->init_lcd_(this->extra_init_sequence_.data());
switch (this->pixel_mode_) { switch (this->pixel_mode_) {
case PIXEL_MODE_16: case PIXEL_MODE_16:
if (this->is_18bitdisplay_) { if (this->is_18bitdisplay_) {
@ -405,42 +405,29 @@ void ILI9XXXDisplay::reset_() {
} }
} }
void ILI9XXXDisplay::init_lcd(const uint8_t *addr) { void ILI9XXXDisplay::init_lcd_(const uint8_t *addr) {
if (addr == nullptr) if (addr == nullptr)
return; return;
uint8_t cmd, x, num_args; uint8_t cmd, x, num_args;
while ((cmd = *addr++) != 0) { while ((cmd = *addr++) != 0) {
x = *addr++; x = *addr++;
if (cmd == ILI9XXX_DELAY) { if (x == ILI9XXX_DELAY_FLAG) {
ESP_LOGD(TAG, "Delay %dms", x); cmd &= 0x7F;
delay(x); ESP_LOGV(TAG, "Delay %dms", cmd);
delay(cmd);
} else { } else {
num_args = x & 0x7F; num_args = x & 0x7F;
ESP_LOGD(TAG, "Command %02X, length %d, bits %02X", cmd, num_args, *addr); ESP_LOGV(TAG, "Command %02X, length %d, bits %02X", cmd, num_args, *addr);
this->send_command(cmd, addr, num_args); this->send_command(cmd, addr, num_args);
addr += num_args; addr += num_args;
if (x & 0x80) { if (x & 0x80) {
ESP_LOGD(TAG, "Delay 150ms"); ESP_LOGV(TAG, "Delay 150ms");
delay(150); // NOLINT delay(150); // NOLINT
} }
} }
} }
} }
void ILI9XXXGC9A01A::init_lcd(const uint8_t *addr) {
if (addr == nullptr)
return;
uint8_t cmd, x, num_args;
while ((cmd = *addr++) != 0) {
x = *addr++;
num_args = x & 0x7F;
this->send_command(cmd, addr, num_args);
addr += num_args;
if (x & 0x80)
delay(150); // NOLINT
}
}
// Tell the display controller where we want to draw pixels. // Tell the display controller where we want to draw pixels.
void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
x1 += this->offset_x_; x1 += this->offset_x_;

View file

@ -33,7 +33,9 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
uint8_t cmd, num_args, bits; uint8_t cmd, num_args, bits;
const uint8_t *addr = init_sequence; const uint8_t *addr = init_sequence;
while ((cmd = *addr++) != 0) { while ((cmd = *addr++) != 0) {
num_args = *addr++ & 0x7F; num_args = *addr++;
if (num_args == ILI9XXX_DELAY_FLAG)
continue;
bits = *addr; bits = *addr;
switch (cmd) { switch (cmd) {
case ILI9XXX_MADCTL: { case ILI9XXX_MADCTL: {
@ -50,13 +52,10 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
break; break;
} }
case ILI9XXX_DELAY:
continue; // no args to skip
default: default:
break; break;
} }
addr += num_args; addr += (num_args & 0x7F);
} }
} }
@ -109,7 +108,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
virtual void set_madctl(); virtual void set_madctl();
void display_(); void display_();
virtual void init_lcd(const uint8_t *addr); void init_lcd_(const uint8_t *addr);
void set_addr_window_(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2); void set_addr_window_(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2);
void reset_(); void reset_();
@ -269,7 +268,6 @@ class ILI9XXXS3BoxLite : public ILI9XXXDisplay {
class ILI9XXXGC9A01A : public ILI9XXXDisplay { class ILI9XXXGC9A01A : public ILI9XXXDisplay {
public: public:
ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240, true) {} ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240, true) {}
void init_lcd(const uint8_t *addr) override;
}; };
//----------- ILI9XXX_24_TFT display -------------- //----------- ILI9XXX_24_TFT display --------------

View file

@ -372,9 +372,9 @@ static const uint8_t PROGMEM INITCMD_GC9A01A[] = {
static const uint8_t PROGMEM INITCMD_ST7735[] = { static const uint8_t PROGMEM INITCMD_ST7735[] = {
ILI9XXX_SWRESET, 0, // Soft reset, then delay 10ms ILI9XXX_SWRESET, 0, // Soft reset, then delay 10ms
ILI9XXX_DELAY, 10, ILI9XXX_DELAY(10),
ILI9XXX_SLPOUT , 0, // Exit Sleep, delay ILI9XXX_SLPOUT , 0, // Exit Sleep, delay
ILI9XXX_DELAY, 10, ILI9XXX_DELAY(10),
ILI9XXX_PIXFMT , 1, 0x05, ILI9XXX_PIXFMT , 1, 0x05,
ILI9XXX_FRMCTR1, 3, // 4: Frame rate control, 3 args + delay: ILI9XXX_FRMCTR1, 3, // 4: Frame rate control, 3 args + delay:
0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D)
@ -415,9 +415,9 @@ static const uint8_t PROGMEM INITCMD_ST7735[] = {
0x00, 0x00, 0x02, 0x10, 0x00, 0x00, 0x02, 0x10,
ILI9XXX_MADCTL , 1, 0x00, // Memory Access Control, BGR ILI9XXX_MADCTL , 1, 0x00, // Memory Access Control, BGR
ILI9XXX_NORON , 0, ILI9XXX_NORON , 0,
ILI9XXX_DELAY, 10, ILI9XXX_DELAY(10),
ILI9XXX_DISPON , 0, // Display on ILI9XXX_DISPON , 0, // Display on
ILI9XXX_DELAY, 10, ILI9XXX_DELAY(10),
00, // endo of list 00, // endo of list
}; };

View file

@ -1,6 +1,6 @@
import esphome.config_validation as cv import esphome.config_validation as cv
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( CONFIG_SCHEMA = cv.invalid(
"The kalman_combinator sensor has moved.\nPlease use the combination platform instead with type: kalman.\n" "The kalman_combinator sensor has moved.\nPlease use the combination platform instead with type: kalman.\n"
"See https://esphome.io/components/sensor/combination.html" "See https://esphome.io/components/sensor/combination.html"
) )

View file

@ -0,0 +1,33 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import CONF_ID
DEPENDENCIES = ["i2c"]
CODEOWNERS = ["@rnauber"]
MULTI_CONF = True
CONF_M5STACK_8ANGLE_ID = "m5stack_8angle_id"
m5stack_8angle_ns = cg.esphome_ns.namespace("m5stack_8angle")
M5Stack8AngleComponent = m5stack_8angle_ns.class_(
"M5Stack8AngleComponent",
i2c.I2CDevice,
cg.Component,
)
AnalogBits = m5stack_8angle_ns.enum("AnalogBits")
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(M5Stack8AngleComponent),
}
).extend(i2c.i2c_device_schema(0x43))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

View file

@ -0,0 +1,30 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from .. import M5Stack8AngleComponent, m5stack_8angle_ns, CONF_M5STACK_8ANGLE_ID
M5Stack8AngleSwitchBinarySensor = m5stack_8angle_ns.class_(
"M5Stack8AngleSwitchBinarySensor",
binary_sensor.BinarySensor,
cg.PollingComponent,
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent),
}
)
.extend(binary_sensor.binary_sensor_schema(M5Stack8AngleSwitchBinarySensor))
.extend(cv.polling_component_schema("10s"))
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID])
sens = await binary_sensor.new_binary_sensor(config)
cg.add(sens.set_parent(hub))
await cg.register_component(sens, config)

View file

@ -0,0 +1,17 @@
#include "m5stack_8angle_binary_sensor.h"
namespace esphome {
namespace m5stack_8angle {
void M5Stack8AngleSwitchBinarySensor::update() {
int8_t out = this->parent_->read_switch();
if (out == -1) {
this->status_set_warning("Could not read binary sensor state from M5Stack 8Angle.");
return;
}
this->publish_state(out != 0);
this->status_clear_warning();
}
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,19 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/core/component.h"
#include "../m5stack_8angle.h"
namespace esphome {
namespace m5stack_8angle {
class M5Stack8AngleSwitchBinarySensor : public binary_sensor::BinarySensor,
public PollingComponent,
public Parented<M5Stack8AngleComponent> {
public:
void update() override;
};
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,31 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import light
from esphome.const import CONF_OUTPUT_ID
from .. import M5Stack8AngleComponent, m5stack_8angle_ns, CONF_M5STACK_8ANGLE_ID
M5Stack8AngleLightsComponent = m5stack_8angle_ns.class_(
"M5Stack8AngleLightOutput",
light.AddressableLight,
)
CONFIG_SCHEMA = cv.All(
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
{
cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent),
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(M5Stack8AngleLightsComponent),
}
)
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID])
lights = cg.new_Pvariable(config[CONF_OUTPUT_ID])
await light.register_light(lights, config)
await cg.register_component(lights, config)
cg.add(lights.set_parent(hub))

View file

@ -0,0 +1,45 @@
#include "m5stack_8angle_light.h"
#include "esphome/core/log.h"
namespace esphome {
namespace m5stack_8angle {
static const char *const TAG = "m5stack_8angle.light";
void M5Stack8AngleLightOutput::setup() {
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->buf_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED);
if (this->buf_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate buffer of size %u", M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED);
this->mark_failed();
return;
};
memset(this->buf_, 0xFF, M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED);
this->effect_data_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS);
if (this->effect_data_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate effect data of size %u", M5STACK_8ANGLE_NUM_LEDS);
this->mark_failed();
return;
};
memset(this->effect_data_, 0x00, M5STACK_8ANGLE_NUM_LEDS);
}
void M5Stack8AngleLightOutput::write_state(light::LightState *state) {
for (int i = 0; i < M5STACK_8ANGLE_NUM_LEDS;
i++) { // write one LED at a time, otherwise the message will be truncated
this->parent_->write_register(M5STACK_8ANGLE_REGISTER_RGB_24B + i * M5STACK_8ANGLE_BYTES_PER_LED,
this->buf_ + i * M5STACK_8ANGLE_BYTES_PER_LED, M5STACK_8ANGLE_BYTES_PER_LED);
}
}
light::ESPColorView M5Stack8AngleLightOutput::get_view_internal(int32_t index) const {
size_t pos = index * M5STACK_8ANGLE_BYTES_PER_LED;
// red, green, blue, white, effect_data, color_correction
return {this->buf_ + pos, this->buf_ + pos + 1, this->buf_ + pos + 2,
nullptr, this->effect_data_ + index, &this->correction_};
}
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,37 @@
#pragma once
#include "esphome/components/light/addressable_light.h"
#include "esphome/components/light/light_output.h"
#include "../m5stack_8angle.h"
namespace esphome {
namespace m5stack_8angle {
static const uint8_t M5STACK_8ANGLE_NUM_LEDS = 9;
static const uint8_t M5STACK_8ANGLE_BYTES_PER_LED = 4;
class M5Stack8AngleLightOutput : public light::AddressableLight, public Parented<M5Stack8AngleComponent> {
public:
void setup() override;
void write_state(light::LightState *state) override;
int32_t size() const override { return M5STACK_8ANGLE_NUM_LEDS; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
traits.set_supported_color_modes({light::ColorMode::RGB});
return traits;
};
void clear_effect_data() override { memset(this->effect_data_, 0x00, M5STACK_8ANGLE_NUM_LEDS); };
protected:
light::ESPColorView get_view_internal(int32_t index) const override;
uint8_t *buf_{nullptr};
uint8_t *effect_data_{nullptr};
};
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,74 @@
#include "m5stack_8angle.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace m5stack_8angle {
static const char *const TAG = "m5stack_8angle";
void M5Stack8AngleComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up M5STACK_8ANGLE...");
i2c::ErrorCode err;
err = this->read(nullptr, 0);
if (err != i2c::NO_ERROR) {
ESP_LOGE(TAG, "I2C error %02X...", err);
this->mark_failed();
return;
};
err = this->read_register(M5STACK_8ANGLE_REGISTER_FW_VERSION, &this->fw_version_, 1);
if (err != i2c::NO_ERROR) {
ESP_LOGE(TAG, "I2C error %02X...", err);
this->mark_failed();
return;
};
}
void M5Stack8AngleComponent::dump_config() {
ESP_LOGCONFIG(TAG, "M5STACK_8ANGLE:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " Firmware version: %d ", this->fw_version_);
}
float M5Stack8AngleComponent::read_knob_pos(uint8_t channel, AnalogBits bits) {
int32_t raw_pos = this->read_knob_pos_raw(channel, bits);
if (raw_pos == -1) {
return NAN;
}
return (float) raw_pos / ((1 << bits) - 1);
}
int32_t M5Stack8AngleComponent::read_knob_pos_raw(uint8_t channel, AnalogBits bits) {
uint16_t knob_pos = 0;
i2c::ErrorCode err;
if (bits == BITS_8) {
err = this->read_register(M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_8B + channel, (uint8_t *) &knob_pos, 1);
} else if (bits == BITS_12) {
err = this->read_register(M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_12B + (channel * 2), (uint8_t *) &knob_pos, 2);
} else {
ESP_LOGE(TAG, "Invalid number of bits: %d", bits);
return -1;
}
if (err == i2c::NO_ERROR) {
return knob_pos;
} else {
return -1;
}
}
int8_t M5Stack8AngleComponent::read_switch() {
uint8_t out;
i2c::ErrorCode err = this->read_register(M5STACK_8ANGLE_REGISTER_DIGITAL_INPUT, (uint8_t *) &out, 1);
if (err == i2c::NO_ERROR) {
return out ? 1 : 0;
} else {
return -1;
}
}
float M5Stack8AngleComponent::get_setup_priority() const { return setup_priority::DATA; }
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,34 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
namespace esphome {
namespace m5stack_8angle {
static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_12B = 0x00;
static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_8B = 0x10;
static const uint8_t M5STACK_8ANGLE_REGISTER_DIGITAL_INPUT = 0x20;
static const uint8_t M5STACK_8ANGLE_REGISTER_RGB_24B = 0x30;
static const uint8_t M5STACK_8ANGLE_REGISTER_FW_VERSION = 0xFE;
enum AnalogBits : uint8_t {
BITS_8 = 8,
BITS_12 = 12,
};
class M5Stack8AngleComponent : public i2c::I2CDevice, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
float read_knob_pos(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8);
int32_t read_knob_pos_raw(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8);
int8_t read_switch();
protected:
uint8_t fw_version_;
};
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,66 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_BIT_DEPTH,
CONF_CHANNEL,
CONF_RAW,
ICON_ROTATE_RIGHT,
STATE_CLASS_MEASUREMENT,
)
from .. import (
AnalogBits,
M5Stack8AngleComponent,
m5stack_8angle_ns,
CONF_M5STACK_8ANGLE_ID,
)
M5Stack8AngleKnobSensor = m5stack_8angle_ns.class_(
"M5Stack8AngleKnobSensor",
sensor.Sensor,
cg.PollingComponent,
)
BIT_DEPTHS = {
8: AnalogBits.BITS_8,
12: AnalogBits.BITS_12,
}
_validate_bits = cv.float_with_unit("bits", "bit")
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(M5Stack8AngleKnobSensor),
cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent),
cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=8),
cv.Optional(CONF_BIT_DEPTH, default="8bit"): cv.All(
_validate_bits, cv.enum(BIT_DEPTHS)
),
cv.Optional(CONF_RAW, default=False): cv.boolean,
}
)
.extend(
sensor.sensor_schema(
M5Stack8AngleKnobSensor,
accuracy_decimals=2,
icon=ICON_ROTATE_RIGHT,
state_class=STATE_CLASS_MEASUREMENT,
)
)
.extend(cv.polling_component_schema("10s"))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_M5STACK_8ANGLE_ID])
cg.add(var.set_channel(config[CONF_CHANNEL] - 1))
cg.add(var.set_bit_depth(BIT_DEPTHS[config[CONF_BIT_DEPTH]]))
cg.add(var.set_raw(config[CONF_RAW]))

View file

@ -0,0 +1,24 @@
#include "m5stack_8angle_sensor.h"
namespace esphome {
namespace m5stack_8angle {
void M5Stack8AngleKnobSensor::update() {
if (this->parent_ != nullptr) {
int32_t raw_pos = this->parent_->read_knob_pos_raw(this->channel_, this->bits_);
if (raw_pos == -1) {
this->status_set_warning("Could not read knob position from M5Stack 8Angle.");
return;
}
if (this->raw_) {
this->publish_state(raw_pos);
} else {
float knob_pos = (float) raw_pos / ((1 << this->bits_) - 1);
this->publish_state(knob_pos);
}
this->status_clear_warning();
};
}
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,27 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "../m5stack_8angle.h"
namespace esphome {
namespace m5stack_8angle {
class M5Stack8AngleKnobSensor : public sensor::Sensor,
public PollingComponent,
public Parented<M5Stack8AngleComponent> {
public:
void update() override;
void set_channel(uint8_t channel) { this->channel_ = channel; };
void set_bit_depth(AnalogBits bits) { this->bits_ = bits; };
void set_raw(bool raw) { this->raw_ = raw; };
protected:
uint8_t channel_;
AnalogBits bits_;
bool raw_;
};
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -1,8 +1,16 @@
import binascii import binascii
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation
from esphome.components import modbus from esphome.components import modbus
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_NAME, CONF_LAMBDA, CONF_OFFSET from esphome.const import (
CONF_ADDRESS,
CONF_ID,
CONF_NAME,
CONF_LAMBDA,
CONF_OFFSET,
CONF_TRIGGER_ID,
)
from esphome.cpp_helpers import logging from esphome.cpp_helpers import logging
from .const import ( from .const import (
CONF_BITMASK, CONF_BITMASK,
@ -12,6 +20,7 @@ from .const import (
CONF_CUSTOM_COMMAND, CONF_CUSTOM_COMMAND,
CONF_FORCE_NEW_RANGE, CONF_FORCE_NEW_RANGE,
CONF_MODBUS_CONTROLLER_ID, CONF_MODBUS_CONTROLLER_ID,
CONF_ON_COMMAND_SENT,
CONF_REGISTER_COUNT, CONF_REGISTER_COUNT,
CONF_REGISTER_TYPE, CONF_REGISTER_TYPE,
CONF_RESPONSE_SIZE, CONF_RESPONSE_SIZE,
@ -97,6 +106,10 @@ TYPE_REGISTER_MAP = {
"FP32_R": 2, "FP32_R": 2,
} }
ModbusCommandSentTrigger = modbus_controller_ns.class_(
"ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_)
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ModbusServerRegisterSchema = cv.Schema( ModbusServerRegisterSchema = cv.Schema(
@ -120,13 +133,19 @@ CONFIG_SCHEMA = cv.All(
cv.Optional( cv.Optional(
CONF_SERVER_REGISTERS, CONF_SERVER_REGISTERS,
): cv.ensure_list(ModbusServerRegisterSchema), ): cv.ensure_list(ModbusServerRegisterSchema),
cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ModbusCommandSentTrigger
),
}
),
} }
) )
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
.extend(modbus.modbus_device_schema(0x01)) .extend(modbus.modbus_device_schema(0x01))
) )
ModbusItemBaseSchema = cv.Schema( ModbusItemBaseSchema = cv.Schema(
{ {
cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController),
@ -254,6 +273,11 @@ async def to_code(config):
) )
) )
await register_modbus_device(var, config) await register_modbus_device(var, config)
for conf in config.get(CONF_ON_COMMAND_SENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(int, "function_code"), (int, "address")], conf
)
async def register_modbus_device(var, config): async def register_modbus_device(var, config):

View file

@ -0,0 +1,19 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/modbus_controller/modbus_controller.h"
namespace esphome {
namespace modbus_controller {
class ModbusCommandSentTrigger : public Trigger<int, int> {
public:
ModbusCommandSentTrigger(ModbusController *a_modbuscontroller) {
a_modbuscontroller->add_on_command_sent_callback(
[this](int function_code, int address) { this->trigger(function_code, address); });
}
};
} // namespace modbus_controller
} // namespace esphome

View file

@ -6,6 +6,7 @@ CONF_CUSTOM_COMMAND = "custom_command"
CONF_FORCE_NEW_RANGE = "force_new_range" CONF_FORCE_NEW_RANGE = "force_new_range"
CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id" CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id"
CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode" CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode"
CONF_ON_COMMAND_SENT = "on_command_sent"
CONF_RAW_ENCODE = "raw_encode" CONF_RAW_ENCODE = "raw_encode"
CONF_REGISTER_COUNT = "register_count" CONF_REGISTER_COUNT = "register_count"
CONF_REGISTER_TYPE = "register_type" CONF_REGISTER_TYPE = "register_type"

View file

@ -43,7 +43,11 @@ bool ModbusController::send_next_command_() {
ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_, ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_,
command->register_address, command->register_count); command->register_address, command->register_count);
command->send(); command->send();
this->last_command_timestamp_ = millis(); this->last_command_timestamp_ = millis();
this->command_sent_callback_.call((int) command->function_code, command->register_address);
// remove from queue if no handler is defined // remove from queue if no handler is defined
if (!command->on_data_func) { if (!command->on_data_func) {
command_queue_.pop_front(); command_queue_.pop_front();
@ -659,5 +663,9 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
return value; return value;
} }
void ModbusController::add_on_command_sent_callback(std::function<void(int, int)> &&callback) {
this->command_sent_callback_.add(std::move(callback));
}
} // namespace modbus_controller } // namespace modbus_controller
} // namespace esphome } // namespace esphome

View file

@ -456,6 +456,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
size_t get_command_queue_length() { return command_queue_.size(); } size_t get_command_queue_length() { return command_queue_.size(); }
/// get if the module is offline, didn't respond the last command /// get if the module is offline, didn't respond the last command
bool get_module_offline() { return module_offline_; } bool get_module_offline() { return module_offline_; }
/// Set callback for commands
void add_on_command_sent_callback(std::function<void(int, int)> &&callback);
protected: protected:
/// parse sensormap_ and create range of sequential addresses /// parse sensormap_ and create range of sequential addresses
@ -488,6 +490,7 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
bool module_offline_; bool module_offline_;
/// how many updates to skip if module is offline /// how many updates to skip if module is offline
uint16_t offline_skip_updates_; uint16_t offline_skip_updates_;
CallbackManager<void(int, int)> command_sent_callback_{};
}; };
/** Convert vector<uint8_t> response payload to float. /** Convert vector<uint8_t> response payload to float.

View file

@ -61,6 +61,7 @@ def AUTO_LOAD():
return ["json"] return ["json"]
CONF_DISCOVER_IP = "discover_ip"
CONF_IDF_SEND_ASYNC = "idf_send_async" CONF_IDF_SEND_ASYNC = "idf_send_async"
CONF_SKIP_CERT_CN_CHECK = "skip_cert_cn_check" CONF_SKIP_CERT_CN_CHECK = "skip_cert_cn_check"
@ -225,6 +226,7 @@ CONFIG_SCHEMA = cv.All(
cv.boolean, cv.one_of("CLEAN", upper=True) cv.boolean, cv.one_of("CLEAN", upper=True)
), ),
cv.Optional(CONF_DISCOVERY_RETAIN, default=True): cv.boolean, cv.Optional(CONF_DISCOVERY_RETAIN, default=True): cv.boolean,
cv.Optional(CONF_DISCOVER_IP, default=True): cv.boolean,
cv.Optional( cv.Optional(
CONF_DISCOVERY_PREFIX, default="homeassistant" CONF_DISCOVERY_PREFIX, default="homeassistant"
): cv.publish_topic, ): cv.publish_topic,
@ -328,8 +330,12 @@ async def to_code(config):
discovery_prefix = config[CONF_DISCOVERY_PREFIX] discovery_prefix = config[CONF_DISCOVERY_PREFIX]
discovery_unique_id_generator = config[CONF_DISCOVERY_UNIQUE_ID_GENERATOR] discovery_unique_id_generator = config[CONF_DISCOVERY_UNIQUE_ID_GENERATOR]
discovery_object_id_generator = config[CONF_DISCOVERY_OBJECT_ID_GENERATOR] discovery_object_id_generator = config[CONF_DISCOVERY_OBJECT_ID_GENERATOR]
discover_ip = config[CONF_DISCOVER_IP]
if not discovery: if not discovery:
discovery_prefix = ""
if not discovery and not discover_ip:
cg.add(var.disable_discovery()) cg.add(var.disable_discovery())
elif discovery == "CLEAN": elif discovery == "CLEAN":
cg.add( cg.add(
@ -338,6 +344,7 @@ async def to_code(config):
discovery_unique_id_generator, discovery_unique_id_generator,
discovery_object_id_generator, discovery_object_id_generator,
discovery_retain, discovery_retain,
discover_ip,
True, True,
) )
) )
@ -348,6 +355,7 @@ async def to_code(config):
discovery_unique_id_generator, discovery_unique_id_generator,
discovery_object_id_generator, discovery_object_id_generator,
discovery_retain, discovery_retain,
discover_ip,
) )
) )

View file

@ -66,7 +66,7 @@ void MQTTClientComponent::setup() {
} }
#endif #endif
if (this->is_discovery_enabled()) { if (this->is_discovery_ip_enabled()) {
this->subscribe( this->subscribe(
"esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, "esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); },
2); 2);
@ -82,7 +82,7 @@ void MQTTClientComponent::setup() {
} }
void MQTTClientComponent::send_device_info_() { void MQTTClientComponent::send_device_info_() {
if (!this->is_connected() or !this->is_discovery_enabled()) { if (!this->is_connected() or !this->is_discovery_ip_enabled()) {
return; return;
} }
std::string topic = "esphome/discover/"; std::string topic = "esphome/discover/";
@ -99,6 +99,9 @@ void MQTTClientComponent::send_device_info_() {
} }
} }
root["name"] = App.get_name(); root["name"] = App.get_name();
if (!App.get_friendly_name().empty()) {
root["friendly_name"] = App.get_friendly_name();
}
#ifdef USE_API #ifdef USE_API
root["port"] = api::global_api_server->get_port(); root["port"] = api::global_api_server->get_port();
#endif #endif
@ -130,6 +133,10 @@ void MQTTClientComponent::send_device_info_() {
#ifdef USE_DASHBOARD_IMPORT #ifdef USE_DASHBOARD_IMPORT
root["package_import_url"] = dashboard_import::get_package_import_url(); root["package_import_url"] = dashboard_import::get_package_import_url();
#endif #endif
#ifdef USE_API_NOISE
root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
#endif
}, },
2, this->discovery_info_.retain); 2, this->discovery_info_.retain);
} }
@ -140,6 +147,9 @@ void MQTTClientComponent::dump_config() {
this->ip_.str().c_str()); this->ip_.str().c_str());
ESP_LOGCONFIG(TAG, " Username: " LOG_SECRET("'%s'"), this->credentials_.username.c_str()); ESP_LOGCONFIG(TAG, " Username: " LOG_SECRET("'%s'"), this->credentials_.username.c_str());
ESP_LOGCONFIG(TAG, " Client ID: " LOG_SECRET("'%s'"), this->credentials_.client_id.c_str()); ESP_LOGCONFIG(TAG, " Client ID: " LOG_SECRET("'%s'"), this->credentials_.client_id.c_str());
if (this->is_discovery_ip_enabled()) {
ESP_LOGCONFIG(TAG, " Discovery IP enabled");
}
if (!this->discovery_info_.prefix.empty()) { if (!this->discovery_info_.prefix.empty()) {
ESP_LOGCONFIG(TAG, " Discovery prefix: '%s'", this->discovery_info_.prefix.c_str()); ESP_LOGCONFIG(TAG, " Discovery prefix: '%s'", this->discovery_info_.prefix.c_str());
ESP_LOGCONFIG(TAG, " Discovery retain: %s", YESNO(this->discovery_info_.retain)); ESP_LOGCONFIG(TAG, " Discovery retain: %s", YESNO(this->discovery_info_.retain));
@ -581,6 +591,7 @@ void MQTTClientComponent::disable_shutdown_message() {
this->recalculate_availability_(); this->recalculate_availability_();
} }
bool MQTTClientComponent::is_discovery_enabled() const { return !this->discovery_info_.prefix.empty(); } bool MQTTClientComponent::is_discovery_enabled() const { return !this->discovery_info_.prefix.empty(); }
bool MQTTClientComponent::is_discovery_ip_enabled() const { return this->discovery_info_.discover_ip; }
const Availability &MQTTClientComponent::get_availability() { return this->availability_; } const Availability &MQTTClientComponent::get_availability() { return this->availability_; }
void MQTTClientComponent::recalculate_availability_() { void MQTTClientComponent::recalculate_availability_() {
if (this->birth_message_.topic.empty() || this->birth_message_.topic != this->last_will_.topic) { if (this->birth_message_.topic.empty() || this->birth_message_.topic != this->last_will_.topic) {
@ -606,8 +617,9 @@ void MQTTClientComponent::set_shutdown_message(MQTTMessage &&message) { this->sh
void MQTTClientComponent::set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator, void MQTTClientComponent::set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator,
MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain,
bool clean) { bool discover_ip, bool clean) {
this->discovery_info_.prefix = std::move(prefix); this->discovery_info_.prefix = std::move(prefix);
this->discovery_info_.discover_ip = discover_ip;
this->discovery_info_.unique_id_generator = unique_id_generator; this->discovery_info_.unique_id_generator = unique_id_generator;
this->discovery_info_.object_id_generator = object_id_generator; this->discovery_info_.object_id_generator = object_id_generator;
this->discovery_info_.retain = retain; this->discovery_info_.retain = retain;

View file

@ -79,6 +79,7 @@ enum MQTTDiscoveryObjectIdGenerator {
struct MQTTDiscoveryInfo { struct MQTTDiscoveryInfo {
std::string prefix; ///< The Home Assistant discovery prefix. Empty means disabled. std::string prefix; ///< The Home Assistant discovery prefix. Empty means disabled.
bool retain; ///< Whether to retain discovery messages. bool retain; ///< Whether to retain discovery messages.
bool discover_ip; ///< Enable the Home Assistant device discovery.
bool clean; bool clean;
MQTTDiscoveryUniqueIdGenerator unique_id_generator; MQTTDiscoveryUniqueIdGenerator unique_id_generator;
MQTTDiscoveryObjectIdGenerator object_id_generator; MQTTDiscoveryObjectIdGenerator object_id_generator;
@ -122,12 +123,14 @@ class MQTTClientComponent : public Component {
* @param retain Whether to retain discovery messages. * @param retain Whether to retain discovery messages.
*/ */
void set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator, void set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator,
MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, bool clean = false); MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, bool discover_ip,
bool clean = false);
/// Get Home Assistant discovery info. /// Get Home Assistant discovery info.
const MQTTDiscoveryInfo &get_discovery_info() const; const MQTTDiscoveryInfo &get_discovery_info() const;
/// Globally disable Home Assistant discovery. /// Globally disable Home Assistant discovery.
void disable_discovery(); void disable_discovery();
bool is_discovery_enabled() const; bool is_discovery_enabled() const;
bool is_discovery_ip_enabled() const;
#if ASYNC_TCP_SSL_ENABLED #if ASYNC_TCP_SSL_ENABLED
/** Add a SSL fingerprint to use for TCP SSL connections to the MQTT broker. /** Add a SSL fingerprint to use for TCP SSL connections to the MQTT broker.
@ -290,6 +293,7 @@ class MQTTClientComponent : public Component {
MQTTDiscoveryInfo discovery_info_{ MQTTDiscoveryInfo discovery_info_{
.prefix = "homeassistant", .prefix = "homeassistant",
.retain = true, .retain = true,
.discover_ip = true,
.clean = false, .clean = false,
.unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR,
.object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR, .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR,

View file

@ -27,7 +27,7 @@ bool SmlFile::setup_node(SmlNode *node) {
uint8_t parse_length = length; uint8_t parse_length = length;
if (has_extended_length) { if (has_extended_length) {
length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f); length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f);
parse_length = length - 1; parse_length = length;
this->pos_ += 1; this->pos_ += 1;
} }
@ -37,7 +37,9 @@ bool SmlFile::setup_node(SmlNode *node) {
node->type = type & 0x07; node->type = type & 0x07;
node->nodes.clear(); node->nodes.clear();
node->value_bytes.clear(); node->value_bytes.clear();
if (this->buffer_[this->pos_] == 0x00) { // end of message
// if the list is a has_extended_length list with e.g. 16 elements this is a 0x00 byte but not the end of message
if (!has_extended_length && this->buffer_[this->pos_] == 0x00) { // end of message
this->pos_ += 1; this->pos_ += 1;
} else if (is_list) { // list } else if (is_list) { // list
this->pos_ += 1; this->pos_ += 1;

View file

@ -832,7 +832,6 @@ def time_of_day(value):
def date_time(date: bool, time: bool): def date_time(date: bool, time: bool):
pattern_str = r"^" # Start of string pattern_str = r"^" # Start of string
if date: if date:
pattern_str += r"\d{4}-\d{1,2}-\d{1,2}" pattern_str += r"\d{4}-\d{1,2}-\d{1,2}"
@ -2038,6 +2037,7 @@ def require_framework_version(
esp32_arduino=None, esp32_arduino=None,
esp8266_arduino=None, esp8266_arduino=None,
rp2040_arduino=None, rp2040_arduino=None,
host=None,
max_version=False, max_version=False,
extra_message=None, extra_message=None,
): ):
@ -2072,6 +2072,13 @@ def require_framework_version(
msg += f". {extra_message}" msg += f". {extra_message}"
raise Invalid(msg) raise Invalid(msg)
required = rp2040_arduino required = rp2040_arduino
elif CORE.is_host and framework == "host":
if host is None:
msg = "This feature is incompatible with host platform"
if extra_message:
msg += f". {extra_message}"
raise Invalid(msg)
required = host
else: else:
raise Invalid( raise Invalid(
f""" f"""

View file

@ -0,0 +1,12 @@
i2c:
- id: i2c_apds9306
scl: ${scl_pin}
sda: ${sda_pin}
sensor:
- platform: apds9306
name: "APDS9306 Light Level"
gain: 3
bit_width: 16
measurement_rate: 2000ms
update_interval: 60s

View file

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO22
sda_pin: GPIO21
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO22
sda_pin: GPIO21
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View file

@ -3,6 +3,13 @@ remote_transmitter:
carrier_duty_percent: 50% carrier_duty_percent: 50%
climate: climate:
- platform: heatpumpir
protocol: mitsubishi_heavy_zm
horizontal_default: left
vertical_default: up
name: HeatpumpIR Climate
min_temperature: 18
max_temperature: 30
- platform: heatpumpir - platform: heatpumpir
protocol: daikin protocol: daikin
horizontal_default: mleft horizontal_default: mleft

View file

@ -0,0 +1,30 @@
i2c:
sda: 0
scl: 1
id: bus_external
m5stack_8angle:
i2c_id: bus_external
id: m5stack_8angle_base
light:
- platform: m5stack_8angle
m5stack_8angle_id: m5stack_8angle_base
id: m8_angle_leds
name: Lights
effects:
- addressable_scan:
sensor:
- platform: m5stack_8angle
m5stack_8angle_id: m5stack_8angle_base
channel: 1
name: Knob 1
- platform: m5stack_8angle
m5stack_8angle_id: m5stack_8angle_base
channel: 2
name: Knob 2
binary_sensor:
- platform: m5stack_8angle
m5stack_8angle_id: m5stack_8angle_base
name: Switch

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml