1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-02 03:12:20 +01:00

Merge branch 'dev' into vornado-ir

This commit is contained in:
Jordan Zucker
2025-01-31 12:48:23 -08:00
95 changed files with 1867 additions and 435 deletions

View File

@@ -46,7 +46,7 @@ runs:
- name: Build and push to ghcr by digest - name: Build and push to ghcr by digest
id: build-ghcr id: build-ghcr
uses: docker/build-push-action@v6.12.0 uses: docker/build-push-action@v6.13.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false
@@ -72,7 +72,7 @@ runs:
- name: Build and push to dockerhub by digest - name: Build and push to dockerhub by digest
id: build-dockerhub id: build-dockerhub
uses: docker/build-push-action@v6.12.0 uses: docker/build-push-action@v6.13.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false

View File

@@ -17,7 +17,7 @@ runs:
steps: steps:
- name: Set up Python ${{ inputs.python-version }} - name: Set up Python ${{ inputs.python-version }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment

View File

@@ -23,7 +23,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: "3.11" python-version: "3.11"

View File

@@ -42,7 +42,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: "3.9" python-version: "3.9"
- name: Set up Docker Buildx - name: Set up Docker Buildx

View File

@@ -42,7 +42,7 @@ jobs:
run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment

View File

@@ -53,7 +53,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: "3.x" python-version: "3.x"
- name: Set up python environment - name: Set up python environment
@@ -65,7 +65,7 @@ jobs:
pip3 install build pip3 install build
python3 -m build python3 -m build
- name: Publish - name: Publish
uses: pypa/gh-action-pypi-publish@v1.12.3 uses: pypa/gh-action-pypi-publish@v1.12.4
deploy-docker: deploy-docker:
name: Build ESPHome ${{ matrix.platform }} name: Build ESPHome ${{ matrix.platform }}
@@ -85,7 +85,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: "3.9" python-version: "3.9"

View File

@@ -22,7 +22,7 @@ jobs:
path: lib/home-assistant path: lib/home-assistant
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: 3.12 python-version: 3.12

View File

@@ -148,6 +148,7 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core esphome/components/esp8266/* @esphome/core
esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/event/* @nohat esphome/components/event/* @nohat
esphome/components/event_emitter/* @Rapsssito
esphome/components/exposure_notifications/* @OttoWinter esphome/components/exposure_notifications/* @OttoWinter
esphome/components/ezo/* @ssieb esphome/components/ezo/* @ssieb
esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/ezo_pmp/* @carlos-sarmiento

View File

@@ -9,8 +9,6 @@ static const char *const TAG = "ads1115";
static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00; static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00;
static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
static const uint8_t ADS1115_DATA_RATE_860_SPS = 0b111; // 3300_SPS for ADS1015
void ADS1115Component::setup() { void ADS1115Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up ADS1115..."); ESP_LOGCONFIG(TAG, "Setting up ADS1115...");
uint16_t value; uint16_t value;
@@ -43,9 +41,9 @@ void ADS1115Component::setup() {
config |= 0b0000000100000000; config |= 0b0000000100000000;
} }
// Set data rate - 860 samples per second (we're in singleshot mode) // Set data rate - 860 samples per second
// 0bxxxxxxxx100xxxxx // 0bxxxxxxxx100xxxxx
config |= ADS1115_DATA_RATE_860_SPS << 5; config |= ADS1115_860SPS << 5;
// Set comparator mode - hysteresis // Set comparator mode - hysteresis
// 0bxxxxxxxxxxx0xxxx // 0bxxxxxxxxxxx0xxxx
@@ -77,7 +75,7 @@ void ADS1115Component::dump_config() {
} }
} }
float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain,
ADS1115Resolution resolution) { ADS1115Resolution resolution, ADS1115Samplerate samplerate) {
uint16_t config = this->prev_config_; uint16_t config = this->prev_config_;
// Multiplexer // Multiplexer
// 0bxBBBxxxxxxxxxxxx // 0bxBBBxxxxxxxxxxxx
@@ -89,6 +87,11 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1
config &= 0b1111000111111111; config &= 0b1111000111111111;
config |= (gain & 0b111) << 9; config |= (gain & 0b111) << 9;
// Sample rate
// 0bxxxxxxxxBBBxxxxx
config &= 0b1111111100011111;
config |= (samplerate & 0b111) << 5;
if (!this->continuous_mode_) { if (!this->continuous_mode_) {
// Start conversion // Start conversion
config |= 0b1000000000000000; config |= 0b1000000000000000;
@@ -101,8 +104,54 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1
} }
this->prev_config_ = config; this->prev_config_ = config;
// about 1.2 ms with 860 samples per second // Delay calculated as: ceil((1000/SPS)+.5)
if (resolution == ADS1015_12_BITS) {
switch (samplerate) {
case ADS1115_8SPS:
delay(9);
break;
case ADS1115_16SPS:
delay(5);
break;
case ADS1115_32SPS:
delay(3);
break;
case ADS1115_64SPS:
case ADS1115_128SPS:
delay(2); delay(2);
break;
default:
delay(1);
break;
}
} else {
switch (samplerate) {
case ADS1115_8SPS:
delay(126); // NOLINT
break;
case ADS1115_16SPS:
delay(63); // NOLINT
break;
case ADS1115_32SPS:
delay(32);
break;
case ADS1115_64SPS:
delay(17);
break;
case ADS1115_128SPS:
delay(9);
break;
case ADS1115_250SPS:
delay(5);
break;
case ADS1115_475SPS:
delay(3);
break;
case ADS1115_860SPS:
delay(2);
break;
}
}
// in continuous mode, conversion will always be running, rely on the delay // in continuous mode, conversion will always be running, rely on the delay
// to ensure conversion is taking place with the correct settings // to ensure conversion is taking place with the correct settings

View File

@@ -33,6 +33,17 @@ enum ADS1115Resolution {
ADS1015_12_BITS = 12, ADS1015_12_BITS = 12,
}; };
enum ADS1115Samplerate {
ADS1115_8SPS = 0b000,
ADS1115_16SPS = 0b001,
ADS1115_32SPS = 0b010,
ADS1115_64SPS = 0b011,
ADS1115_128SPS = 0b100,
ADS1115_250SPS = 0b101,
ADS1115_475SPS = 0b110,
ADS1115_860SPS = 0b111
};
class ADS1115Component : public Component, public i2c::I2CDevice { class ADS1115Component : public Component, public i2c::I2CDevice {
public: public:
void setup() override; void setup() override;
@@ -42,7 +53,8 @@ class ADS1115Component : public Component, public i2c::I2CDevice {
void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; } void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; }
/// Helper method to request a measurement from a sensor. /// Helper method to request a measurement from a sensor.
float request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution); float request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution,
ADS1115Samplerate samplerate);
protected: protected:
uint16_t prev_config_{0}; uint16_t prev_config_{0};

View File

@@ -5,6 +5,7 @@ from esphome.const import (
CONF_GAIN, CONF_GAIN,
CONF_MULTIPLEXER, CONF_MULTIPLEXER,
CONF_RESOLUTION, CONF_RESOLUTION,
CONF_SAMPLE_RATE,
DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_VOLT, UNIT_VOLT,
@@ -43,6 +44,17 @@ RESOLUTION = {
"12_BITS": ADS1115Resolution.ADS1015_12_BITS, "12_BITS": ADS1115Resolution.ADS1015_12_BITS,
} }
ADS1115Samplerate = ads1115_ns.enum("ADS1115Samplerate")
SAMPLERATE = {
"8": ADS1115Samplerate.ADS1115_8SPS,
"16": ADS1115Samplerate.ADS1115_16SPS,
"32": ADS1115Samplerate.ADS1115_32SPS,
"64": ADS1115Samplerate.ADS1115_64SPS,
"128": ADS1115Samplerate.ADS1115_128SPS,
"250": ADS1115Samplerate.ADS1115_250SPS,
"475": ADS1115Samplerate.ADS1115_475SPS,
"860": ADS1115Samplerate.ADS1115_860SPS,
}
ADS1115Sensor = ads1115_ns.class_( ADS1115Sensor = ads1115_ns.class_(
"ADS1115Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler "ADS1115Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
@@ -64,6 +76,9 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum( cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum(
RESOLUTION, upper=True, space="_" RESOLUTION, upper=True, space="_"
), ),
cv.Optional(CONF_SAMPLE_RATE, default="860"): cv.enum(
SAMPLERATE, string=True
),
} }
) )
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
@@ -79,3 +94,4 @@ async def to_code(config):
cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER]))
cg.add(var.set_gain(config[CONF_GAIN])) cg.add(var.set_gain(config[CONF_GAIN]))
cg.add(var.set_resolution(config[CONF_RESOLUTION])) cg.add(var.set_resolution(config[CONF_RESOLUTION]))
cg.add(var.set_samplerate(config[CONF_SAMPLE_RATE]))

View File

@@ -8,7 +8,7 @@ namespace ads1115 {
static const char *const TAG = "ads1115.sensor"; static const char *const TAG = "ads1115.sensor";
float ADS1115Sensor::sample() { float ADS1115Sensor::sample() {
return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->resolution_); return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->resolution_, this->samplerate_);
} }
void ADS1115Sensor::update() { void ADS1115Sensor::update() {
@@ -24,6 +24,7 @@ void ADS1115Sensor::dump_config() {
ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_); ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_);
ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_); ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_);
ESP_LOGCONFIG(TAG, " Resolution: %u", this->resolution_); ESP_LOGCONFIG(TAG, " Resolution: %u", this->resolution_);
ESP_LOGCONFIG(TAG, " Sample rate: %u", this->samplerate_);
} }
} // namespace ads1115 } // namespace ads1115

View File

@@ -21,6 +21,7 @@ class ADS1115Sensor : public sensor::Sensor,
void set_multiplexer(ADS1115Multiplexer multiplexer) { this->multiplexer_ = multiplexer; } void set_multiplexer(ADS1115Multiplexer multiplexer) { this->multiplexer_ = multiplexer; }
void set_gain(ADS1115Gain gain) { this->gain_ = gain; } void set_gain(ADS1115Gain gain) { this->gain_ = gain; }
void set_resolution(ADS1115Resolution resolution) { this->resolution_ = resolution; } void set_resolution(ADS1115Resolution resolution) { this->resolution_ = resolution; }
void set_samplerate(ADS1115Samplerate samplerate) { this->samplerate_ = samplerate; }
float sample() override; float sample() override;
void dump_config() override; void dump_config() override;
@@ -29,6 +30,7 @@ class ADS1115Sensor : public sensor::Sensor,
ADS1115Multiplexer multiplexer_; ADS1115Multiplexer multiplexer_;
ADS1115Gain gain_; ADS1115Gain gain_;
ADS1115Resolution resolution_; ADS1115Resolution resolution_;
ADS1115Samplerate samplerate_;
}; };
} // namespace ads1115 } // namespace ads1115

View File

@@ -1381,6 +1381,7 @@ message BluetoothConnectionsFreeResponse {
uint32 free = 1; uint32 free = 1;
uint32 limit = 2; uint32 limit = 2;
repeated uint64 allocated = 3;
} }
message BluetoothGATTErrorResponse { message BluetoothGATTErrorResponse {

View File

@@ -6430,6 +6430,10 @@ bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVar
this->limit = value.as_uint32(); this->limit = value.as_uint32();
return true; return true;
} }
case 3: {
this->allocated.push_back(value.as_uint64());
return true;
}
default: default:
return false; return false;
} }
@@ -6437,6 +6441,9 @@ bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVar
void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(1, this->free); buffer.encode_uint32(1, this->free);
buffer.encode_uint32(2, this->limit); buffer.encode_uint32(2, this->limit);
for (auto &it : this->allocated) {
buffer.encode_uint64(3, it, true);
}
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const {
@@ -6451,6 +6458,13 @@ void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const {
sprintf(buffer, "%" PRIu32, this->limit); sprintf(buffer, "%" PRIu32, this->limit);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
for (const auto &it : this->allocated) {
out.append(" allocated: ");
sprintf(buffer, "%llu", it);
out.append(buffer);
out.append("\n");
}
out.append("}"); out.append("}");
} }
#endif #endif

View File

@@ -1624,6 +1624,7 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage {
public: public:
uint32_t free{0}; uint32_t free{0};
uint32_t limit{0}; uint32_t limit{0};
std::vector<uint64_t> allocated{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;

View File

@@ -19,6 +19,7 @@ from .boards import BK72XX_BOARD_PINS, BK72XX_BOARDS
CODEOWNERS = ["@kuba2k2"] CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["libretiny"] AUTO_LOAD = ["libretiny"]
IS_TARGET_PLATFORM = True
COMPONENT_DATA = LibreTinyComponent( COMPONENT_DATA = LibreTinyComponent(
name=COMPONENT_BK72XX, name=COMPONENT_BK72XX,

View File

@@ -11,6 +11,7 @@ from esphome.const import (
DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_SIGNAL_STRENGTH,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_DECIBEL_MILLIWATT, UNIT_DECIBEL_MILLIWATT,
CONF_NOTIFY,
) )
from .. import ble_client_ns from .. import ble_client_ns
@@ -19,7 +20,6 @@ DEPENDENCIES = ["ble_client"]
CONF_DESCRIPTOR_UUID = "descriptor_uuid" CONF_DESCRIPTOR_UUID = "descriptor_uuid"
CONF_NOTIFY = "notify"
CONF_ON_NOTIFY = "on_notify" CONF_ON_NOTIFY = "on_notify"
TYPE_CHARACTERISTIC = "characteristic" TYPE_CHARACTERISTIC = "characteristic"
TYPE_RSSI = "rssi" TYPE_RSSI = "rssi"

View File

@@ -6,6 +6,7 @@ from esphome.const import (
CONF_CHARACTERISTIC_UUID, CONF_CHARACTERISTIC_UUID,
CONF_ID, CONF_ID,
CONF_SERVICE_UUID, CONF_SERVICE_UUID,
CONF_NOTIFY,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
) )
@@ -15,7 +16,6 @@ DEPENDENCIES = ["ble_client"]
CONF_DESCRIPTOR_UUID = "descriptor_uuid" CONF_DESCRIPTOR_UUID = "descriptor_uuid"
CONF_NOTIFY = "notify"
CONF_ON_NOTIFY = "on_notify" CONF_ON_NOTIFY = "on_notify"
adv_data_t = cg.std_vector.template(cg.uint8) adv_data_t = cg.std_vector.template(cg.uint8)

View File

@@ -475,6 +475,11 @@ void BluetoothProxy::send_connections_free() {
api::BluetoothConnectionsFreeResponse call; api::BluetoothConnectionsFreeResponse call;
call.free = this->get_bluetooth_connections_free(); call.free = this->get_bluetooth_connections_free();
call.limit = this->get_bluetooth_connections_limit(); call.limit = this->get_bluetooth_connections_limit();
for (auto *connection : this->connections_) {
if (connection->address_ != 0) {
call.allocated.push_back(connection->address_);
}
}
this->api_connection_->send_bluetooth_connections_free_response(call); this->api_connection_->send_bluetooth_connections_free_response(call);
} }

View File

@@ -115,7 +115,7 @@ CONF_MAX_HUMIDITY = "max_humidity"
CONF_TARGET_HUMIDITY = "target_humidity" CONF_TARGET_HUMIDITY = "target_humidity"
visual_temperature = cv.float_with_unit( visual_temperature = cv.float_with_unit(
"visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" "visual_temperature", "(°C|° C|°|C|°K|° K|K|°F|° F|F)?"
) )

View File

@@ -35,8 +35,8 @@ void DebugComponent::log_partition_info_() {
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
while (it != NULL) { while (it != NULL) {
const esp_partition_t *partition = esp_partition_get(it); const esp_partition_t *partition = esp_partition_get(it);
ESP_LOGCONFIG(TAG, " %-12s %-4d %-8d 0x%08X 0x%08X", partition->label, partition->type, partition->subtype, ESP_LOGCONFIG(TAG, " %-12s %-4d %-8d 0x%08" PRIX32 " 0x%08" PRIX32, partition->label, partition->type,
partition->address, partition->size); partition->subtype, partition->address, partition->size);
it = esp_partition_next(it); it = esp_partition_next(it);
} }
esp_partition_iterator_release(it); esp_partition_iterator_release(it);

View File

@@ -101,7 +101,7 @@ async def setup_display_core_(var, config):
if CONF_ROTATION in config: if CONF_ROTATION in config:
cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
if auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED): if (auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED)) is not None:
# Default to true if pages or lambda is specified. Ideally this would be done during validation, but # Default to true if pages or lambda is specified. Ideally this would be done during validation, but
# the possible schemas are too complex to do this easily. # the possible schemas are too complex to do this easily.
if auto_clear == CONF_UNSPECIFIED: if auto_clear == CONF_UNSPECIFIED:

View File

@@ -64,6 +64,7 @@ from .gpio import esp32_pin_to_code # noqa
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
AUTO_LOAD = ["preferences"] AUTO_LOAD = ["preferences"]
IS_TARGET_PLATFORM = True
CONF_RELEASE = "release" CONF_RELEASE = "release"

View File

@@ -1,3 +1,5 @@
import re
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
@@ -64,6 +66,43 @@ CONFIG_SCHEMA = cv.Schema(
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
bt_uuid16_format = "XXXX"
bt_uuid32_format = "XXXXXXXX"
bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
def bt_uuid(value):
in_value = cv.string_strict(value)
value = in_value.upper()
if len(value) == len(bt_uuid16_format):
pattern = re.compile("^[A-F|0-9]{4,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'"
)
return value
if len(value) == len(bt_uuid32_format):
pattern = re.compile("^[A-F|0-9]{8,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'"
)
return value
if len(value) == len(bt_uuid128_format):
pattern = re.compile(
"^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$"
)
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 128 UUID format: '{in_value}'"
)
return value
raise cv.Invalid(
f"Bluetooth UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format"
)
def validate_variant(_): def validate_variant(_):
variant = get_esp32_variant() variant = get_esp32_variant()
if variant in NO_BLUETOOTH_VARIANTS: if variant in NO_BLUETOOTH_VARIANTS:

View File

@@ -1,37 +1,526 @@
import encodings
from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32_ble from esphome.components import esp32_ble
from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import bt_uuid
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODEL from esphome.config_validation import UNDEFINED
from esphome.const import (
CONF_DATA,
CONF_ESPHOME,
CONF_ID,
CONF_MAX_LENGTH,
CONF_MODEL,
CONF_NOTIFY,
CONF_ON_CONNECT,
CONF_ON_DISCONNECT,
CONF_PROJECT,
CONF_SERVICES,
CONF_TYPE,
CONF_UUID,
CONF_VALUE,
__version__ as ESPHOME_VERSION,
)
from esphome.core import CORE from esphome.core import CORE
from esphome.schema_extractors import SCHEMA_EXTRACT
AUTO_LOAD = ["esp32_ble"] AUTO_LOAD = ["esp32_ble", "bytebuffer", "event_emitter"]
CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
DOMAIN = "esp32_ble_server"
CONF_ADVERTISE = "advertise"
CONF_BROADCAST = "broadcast"
CONF_CHARACTERISTICS = "characteristics"
CONF_DESCRIPTION = "description"
CONF_DESCRIPTORS = "descriptors"
CONF_ENDIANNESS = "endianness"
CONF_FIRMWARE_VERSION = "firmware_version"
CONF_INDICATE = "indicate"
CONF_MANUFACTURER = "manufacturer" CONF_MANUFACTURER = "manufacturer"
CONF_MANUFACTURER_DATA = "manufacturer_data" CONF_MANUFACTURER_DATA = "manufacturer_data"
CONF_ON_WRITE = "on_write"
CONF_READ = "read"
CONF_STRING = "string"
CONF_STRING_ENCODING = "string_encoding"
CONF_WRITE = "write"
CONF_WRITE_NO_RESPONSE = "write_no_response"
# Internal configuration keys
CONF_CHAR_VALUE_ACTION_ID_ = "char_value_action_id_"
# BLE reserverd UUIDs
CCCD_DESCRIPTOR_UUID = 0x2902
CUD_DESCRIPTOR_UUID = 0x2901
DEVICE_INFORMATION_SERVICE_UUID = 0x180A
MANUFACTURER_NAME_CHARACTERISTIC_UUID = 0x2A29
MODEL_CHARACTERISTIC_UUID = 0x2A24
FIRMWARE_VERSION_CHARACTERISTIC_UUID = 0x2A26
# Core key to store the global configuration
KEY_NOTIFY_REQUIRED = "notify_required"
KEY_SET_VALUE = "set_value"
esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server") esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server")
ESPBTUUID_ns = cg.esphome_ns.namespace("esp32_ble").namespace("ESPBTUUID")
BLECharacteristic_ns = esp32_ble_server_ns.namespace("BLECharacteristic")
BLEServer = esp32_ble_server_ns.class_( BLEServer = esp32_ble_server_ns.class_(
"BLEServer", "BLEServer",
cg.Component, cg.Component,
esp32_ble.GATTsEventHandler, esp32_ble.GATTsEventHandler,
cg.Parented.template(esp32_ble.ESP32BLE), cg.Parented.template(esp32_ble.ESP32BLE),
) )
BLEServiceComponent = esp32_ble_server_ns.class_("BLEServiceComponent") esp32_ble_server_automations_ns = esp32_ble_server_ns.namespace(
"esp32_ble_server_automations"
)
BLETriggers_ns = esp32_ble_server_automations_ns.namespace("BLETriggers")
BLEDescriptor = esp32_ble_server_ns.class_("BLEDescriptor")
BLECharacteristic = esp32_ble_server_ns.class_("BLECharacteristic")
BLEService = esp32_ble_server_ns.class_("BLEService")
BLECharacteristicSetValueAction = esp32_ble_server_automations_ns.class_(
"BLECharacteristicSetValueAction", automation.Action
)
BLEDescriptorSetValueAction = esp32_ble_server_automations_ns.class_(
"BLEDescriptorSetValueAction", automation.Action
)
BLECharacteristicNotifyAction = esp32_ble_server_automations_ns.class_(
"BLECharacteristicNotifyAction", automation.Action
)
bytebuffer_ns = cg.esphome_ns.namespace("bytebuffer")
Endianness_ns = bytebuffer_ns.namespace("Endian")
ByteBuffer_ns = bytebuffer_ns.namespace("ByteBuffer")
ByteBuffer = bytebuffer_ns.class_("ByteBuffer")
PROPERTY_MAP = {
CONF_READ: BLECharacteristic_ns.PROPERTY_READ,
CONF_WRITE: BLECharacteristic_ns.PROPERTY_WRITE,
CONF_NOTIFY: BLECharacteristic_ns.PROPERTY_NOTIFY,
CONF_BROADCAST: BLECharacteristic_ns.PROPERTY_BROADCAST,
CONF_INDICATE: BLECharacteristic_ns.PROPERTY_INDICATE,
CONF_WRITE_NO_RESPONSE: BLECharacteristic_ns.PROPERTY_WRITE_NR,
}
class ValueType:
def __init__(self, type_, validator, length):
self.type_ = type_
self.validator = validator
self.length = length
def validate(self, value, encoding):
value = self.validator(value)
if self.type_ == "string":
try:
value.encode(encoding)
except UnicodeEncodeError as e:
raise cv.Invalid(str(e)) from e
return value
VALUE_TYPES = {
type_name: ValueType(type_name, validator, length)
for type_name, validator, length in (
("uint8_t", cv.uint8_t, 1),
("uint16_t", cv.uint16_t, 2),
("uint32_t", cv.uint32_t, 4),
("uint64_t", cv.uint64_t, 8),
("int8_t", cv.int_range(-128, 127), 1),
("int16_t", cv.int_range(-32768, 32767), 2),
("int32_t", cv.int_range(-2147483648, 2147483647), 4),
("int64_t", cv.int_range(-9223372036854775808, 9223372036854775807), 8),
("float", cv.float_, 4),
("double", cv.float_, 8),
("string", cv.string_strict, None), # Length is variable
)
}
def validate_char_on_write(char_config):
if CONF_ON_WRITE in char_config:
if not char_config[CONF_WRITE] and not char_config[CONF_WRITE_NO_RESPONSE]:
raise cv.Invalid(
f"{CONF_ON_WRITE} requires the {CONF_WRITE} or {CONF_WRITE_NO_RESPONSE} property to be set"
)
return char_config
def validate_descriptor(desc_config):
if CONF_ON_WRITE in desc_config:
if not desc_config[CONF_WRITE]:
raise cv.Invalid(
f"{CONF_ON_WRITE} requires the {CONF_WRITE} property to be set"
)
if CONF_MAX_LENGTH not in desc_config:
value = desc_config[CONF_VALUE][CONF_DATA]
if cg.is_template(value):
raise cv.Invalid(
f"Descriptor {desc_config[CONF_UUID]} has a templatable value and the {CONF_MAX_LENGTH} property is not set"
)
if isinstance(value, list):
desc_config[CONF_MAX_LENGTH] = len(value)
elif isinstance(value, str):
desc_config[CONF_MAX_LENGTH] = len(
value.encode(desc_config[CONF_VALUE][CONF_STRING_ENCODING])
)
else:
desc_config[CONF_MAX_LENGTH] = VALUE_TYPES[
desc_config[CONF_VALUE][CONF_TYPE]
].length
return desc_config
def validate_notify_action(config):
# Store the characteristic ID in the global data for the final validation
data = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_NOTIFY_REQUIRED, set())
data.add(config[CONF_ID])
return config
def validate_set_value_action(config):
# Store the characteristic ID in the global data for the final validation
data = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_SET_VALUE, set())
data.add(config[CONF_ID])
return config
def create_description_cud(char_config):
if CONF_DESCRIPTION not in char_config:
return char_config
# If the config displays a description, there cannot be a descriptor with the CUD UUID
for desc in char_config[CONF_DESCRIPTORS]:
if desc[CONF_UUID] == CUD_DESCRIPTOR_UUID:
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has a description, but a CUD descriptor is already present"
)
# Manually add the CUD descriptor
char_config[CONF_DESCRIPTORS].append(
DESCRIPTOR_SCHEMA(
{
CONF_UUID: CUD_DESCRIPTOR_UUID,
CONF_READ: True,
CONF_WRITE: False,
CONF_VALUE: char_config[CONF_DESCRIPTION],
}
)
)
return char_config
def create_notify_cccd(char_config):
if not char_config[CONF_NOTIFY] and not char_config[CONF_INDICATE]:
return char_config
# If the CCCD descriptor is already present, return the config
for desc in char_config[CONF_DESCRIPTORS]:
if desc[CONF_UUID] == CCCD_DESCRIPTOR_UUID:
# Check if the WRITE property is set
if not desc[CONF_WRITE]:
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has notify actions, but the CCCD descriptor does not have the {CONF_WRITE} property set"
)
return char_config
# Manually add the CCCD descriptor
char_config[CONF_DESCRIPTORS].append(
DESCRIPTOR_SCHEMA(
{
CONF_UUID: CCCD_DESCRIPTOR_UUID,
CONF_READ: True,
CONF_WRITE: True,
CONF_MAX_LENGTH: 2,
CONF_VALUE: [0, 0],
}
)
)
return char_config
def create_device_information_service(config):
# If there is already a device information service,
# there cannot be CONF_MODEL, CONF_MANUFACTURER or CONF_FIRMWARE_VERSION properties
for service in config[CONF_SERVICES]:
if service[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID:
if (
CONF_MODEL in config
or CONF_MANUFACTURER in config
or CONF_FIRMWARE_VERSION in config
):
raise cv.Invalid(
"Device information service already present, cannot add manufacturer, model or firmware version"
)
return config
project = CORE.raw_config[CONF_ESPHOME].get(CONF_PROJECT, {})
model = config.get(CONF_MODEL, project.get("name", CORE.data["esp32"]["board"]))
version = config.get(
CONF_FIRMWARE_VERSION, project.get("version", "ESPHome " + ESPHOME_VERSION)
)
# Manually add the device information service
config[CONF_SERVICES].append(
SERVICE_SCHEMA(
{
CONF_UUID: DEVICE_INFORMATION_SERVICE_UUID,
CONF_CHARACTERISTICS: [
{
CONF_UUID: MANUFACTURER_NAME_CHARACTERISTIC_UUID,
CONF_READ: True,
CONF_VALUE: config.get(CONF_MANUFACTURER, "ESPHome"),
},
{
CONF_UUID: MODEL_CHARACTERISTIC_UUID,
CONF_READ: True,
CONF_VALUE: model,
},
{
CONF_UUID: FIRMWARE_VERSION_CHARACTERISTIC_UUID,
CONF_READ: True,
CONF_VALUE: version,
},
],
}
)
)
return config
def final_validate_config(config):
# Check if all characteristics that require notifications have the notify property set
for char_id in CORE.data.get(DOMAIN, {}).get(KEY_NOTIFY_REQUIRED, set()):
# Look for the characteristic in the configuration
char_config = [
char_conf
for service_conf in config[CONF_SERVICES]
for char_conf in service_conf[CONF_CHARACTERISTICS]
if char_conf[CONF_ID] == char_id
][0]
if not char_config[CONF_NOTIFY]:
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has notify actions and the {CONF_NOTIFY} property is not set"
)
for char_id in CORE.data.get(DOMAIN, {}).get(KEY_SET_VALUE, set()):
# Look for the characteristic in the configuration
char_config = [
char_conf
for service_conf in config[CONF_SERVICES]
for char_conf in service_conf[CONF_CHARACTERISTICS]
if char_conf[CONF_ID] == char_id
][0]
if isinstance(char_config.get(CONF_VALUE, {}).get(CONF_DATA), cv.Lambda):
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has both a set_value action and a templated value"
)
return config
def validate_value_type(value_config):
# If the value is a not a templatable, the type must be set
value = value_config[CONF_DATA]
if type_ := value_config.get(CONF_TYPE):
if cg.is_template(value):
raise cv.Invalid(
f'The "{CONF_TYPE}" property is not allowed for templatable values'
)
value_config[CONF_DATA] = VALUE_TYPES[type_].validate(
value, value_config[CONF_STRING_ENCODING]
)
elif isinstance(value, (float, int)):
raise cv.Invalid(
f'The "{CONF_TYPE}" property is required for the value "{value}"'
)
return value_config
def validate_encoding(value):
if value == SCHEMA_EXTRACT:
return cv.one_of("utf-8", "latin-1", "ascii", "utf-16", "utf-32")
value = encodings.normalize_encoding(value)
if not value:
raise cv.Invalid("Invalid encoding")
return value
def value_schema(default_type=UNDEFINED, templatable=True):
data_validators = [
cv.string_strict,
cv.int_,
cv.float_,
cv.All([cv.uint8_t], cv.Length(min=1)),
]
if templatable:
data_validators.append(cv.returning_lambda)
return cv.maybe_simple_value(
cv.All(
{
cv.Required(CONF_DATA): cv.Any(*data_validators),
cv.Optional(CONF_TYPE, default=default_type): cv.one_of(
*VALUE_TYPES, lower=True
),
cv.Optional(CONF_STRING_ENCODING, default="utf_8"): validate_encoding,
cv.Optional(CONF_ENDIANNESS, default="LITTLE"): cv.enum(
{
"LITTLE": Endianness_ns.LITTLE,
"BIG": Endianness_ns.BIG,
}
),
},
validate_value_type,
),
key=CONF_DATA,
)
DESCRIPTOR_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(BLEDescriptor),
cv.Required(CONF_UUID): cv.Any(bt_uuid, cv.hex_uint32_t),
cv.Optional(CONF_READ, default=True): cv.boolean,
cv.Optional(CONF_WRITE, default=True): cv.boolean,
cv.Optional(CONF_ON_WRITE): automation.validate_automation(single=True),
cv.Required(CONF_VALUE): value_schema(templatable=False),
cv.Optional(CONF_MAX_LENGTH): cv.uint16_t,
},
validate_descriptor,
)
CHARACTERISTIC_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLECharacteristic),
cv.Required(CONF_UUID): cv.Any(bt_uuid, cv.hex_uint32_t),
cv.Optional(CONF_VALUE): value_schema(templatable=True),
cv.GenerateID(CONF_CHAR_VALUE_ACTION_ID_): cv.declare_id(
BLECharacteristicSetValueAction
),
cv.Optional(CONF_DESCRIPTORS, default=[]): cv.ensure_list(DESCRIPTOR_SCHEMA),
cv.Optional(CONF_ON_WRITE): automation.validate_automation(single=True),
cv.Optional(CONF_DESCRIPTION): value_schema(
default_type="string", templatable=False
),
},
extra_schemas=[
validate_char_on_write,
create_description_cud,
create_notify_cccd,
],
).extend({cv.Optional(k, default=False): cv.boolean for k in PROPERTY_MAP})
SERVICE_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLEService),
cv.Required(CONF_UUID): cv.Any(bt_uuid, cv.hex_uint32_t),
cv.Optional(CONF_ADVERTISE, default=False): cv.boolean,
cv.Optional(CONF_CHARACTERISTICS, default=[]): cv.ensure_list(
CHARACTERISTIC_SCHEMA
),
}
)
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(BLEServer), cv.GenerateID(): cv.declare_id(BLEServer),
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string, cv.Optional(CONF_MANUFACTURER): value_schema("string", templatable=False),
cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.hex_uint8_t]), cv.Optional(CONF_MODEL): value_schema("string", templatable=False),
cv.Optional(CONF_MODEL): cv.string, cv.Optional(CONF_FIRMWARE_VERSION): value_schema("string", templatable=False),
} cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.uint8_t]),
cv.Optional(CONF_SERVICES, default=[]): cv.ensure_list(SERVICE_SCHEMA),
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True),
},
extra_schemas=[create_device_information_service],
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = final_validate_config
def parse_properties(char_conf):
return sum(
(PROPERTY_MAP[k] for k in char_conf if k in PROPERTY_MAP and char_conf[k]),
start=0,
)
def parse_uuid(uuid):
# If the UUID is a int, use from_uint32
if isinstance(uuid, int):
return ESPBTUUID_ns.from_uint32(uuid)
# Otherwise, use ESPBTUUID_ns.from_raw
return ESPBTUUID_ns.from_raw(uuid)
async def parse_value(value_config, args):
value = value_config[CONF_DATA]
if isinstance(value, cv.Lambda):
return await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
if isinstance(value, str):
value = list(value.encode(value_config[CONF_STRING_ENCODING]))
if isinstance(value, list):
return cg.std_vector.template(cg.uint8)(value)
val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})")
return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS])
def calculate_num_handles(service_config):
total = 1 + len(service_config[CONF_CHARACTERISTICS]) * 2
total += sum(
len(char_conf[CONF_DESCRIPTORS])
for char_conf in service_config[CONF_CHARACTERISTICS]
)
return total
async def to_code_descriptor(descriptor_conf, char_var):
value = await parse_value(descriptor_conf[CONF_VALUE], {})
desc_var = cg.new_Pvariable(
descriptor_conf[CONF_ID],
parse_uuid(descriptor_conf[CONF_UUID]),
descriptor_conf[CONF_MAX_LENGTH],
descriptor_conf[CONF_READ],
descriptor_conf[CONF_WRITE],
)
cg.add(char_var.add_descriptor(desc_var))
cg.add(desc_var.set_value(value))
if CONF_ON_WRITE in descriptor_conf:
on_write_conf = descriptor_conf[CONF_ON_WRITE]
await automation.build_automation(
BLETriggers_ns.create_descriptor_on_write_trigger(desc_var),
[(cg.std_vector.template(cg.uint8), "x"), (cg.uint16, "id")],
on_write_conf,
)
async def to_code_characteristic(service_var, char_conf):
char_var = cg.Pvariable(
char_conf[CONF_ID],
service_var.create_characteristic(
parse_uuid(char_conf[CONF_UUID]),
parse_properties(char_conf),
),
)
if CONF_ON_WRITE in char_conf:
on_write_conf = char_conf[CONF_ON_WRITE]
await automation.build_automation(
BLETriggers_ns.create_characteristic_on_write_trigger(char_var),
[(cg.std_vector.template(cg.uint8), "x"), (cg.uint16, "id")],
on_write_conf,
)
if CONF_VALUE in char_conf:
action_conf = {
CONF_ID: char_conf[CONF_ID],
CONF_VALUE: char_conf[CONF_VALUE],
}
value_action = await ble_server_characteristic_set_value(
action_conf,
char_conf[CONF_CHAR_VALUE_ACTION_ID_],
cg.TemplateArguments(),
{},
)
cg.add(value_action.play())
for descriptor_conf in char_conf[CONF_DESCRIPTORS]:
await to_code_descriptor(descriptor_conf, char_var)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
@@ -42,13 +531,94 @@ async def to_code(config):
cg.add(parent.register_gatts_event_handler(var)) cg.add(parent.register_gatts_event_handler(var))
cg.add(parent.register_ble_status_event_handler(var)) cg.add(parent.register_ble_status_event_handler(var))
cg.add(var.set_parent(parent)) cg.add(var.set_parent(parent))
cg.add(var.set_manufacturer(config[CONF_MANUFACTURER]))
if CONF_MANUFACTURER_DATA in config: if CONF_MANUFACTURER_DATA in config:
cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA])) cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA]))
if CONF_MODEL in config: for service_config in config[CONF_SERVICES]:
cg.add(var.set_model(config[CONF_MODEL])) # Calculate the optimal number of handles based on the number of characteristics and descriptors
num_handles = calculate_num_handles(service_config)
service_var = cg.Pvariable(
service_config[CONF_ID],
var.create_service(
parse_uuid(service_config[CONF_UUID]),
service_config[CONF_ADVERTISE],
num_handles,
),
)
for char_conf in service_config[CONF_CHARACTERISTICS]:
await to_code_characteristic(service_var, char_conf)
if service_config[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID:
cg.add(var.set_device_information_service(service_var))
else:
cg.add(var.enqueue_start_service(service_var))
if CONF_ON_CONNECT in config:
await automation.build_automation(
BLETriggers_ns.create_server_on_connect_trigger(var),
[(cg.uint16, "id")],
config[CONF_ON_CONNECT],
)
if CONF_ON_DISCONNECT in config:
await automation.build_automation(
BLETriggers_ns.create_server_on_disconnect_trigger(var),
[(cg.uint16, "id")],
config[CONF_ON_DISCONNECT],
)
cg.add_define("USE_ESP32_BLE_SERVER") cg.add_define("USE_ESP32_BLE_SERVER")
if CORE.using_esp_idf: if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
@automation.register_action(
"ble_server.characteristic.set_value",
BLECharacteristicSetValueAction,
cv.All(
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(BLECharacteristic),
cv.Required(CONF_VALUE): value_schema(),
}
),
validate_set_value_action,
),
)
async def ble_server_characteristic_set_value(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
value = await parse_value(config[CONF_VALUE], args)
cg.add(var.set_buffer(value))
return var
@automation.register_action(
"ble_server.descriptor.set_value",
BLEDescriptorSetValueAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(BLEDescriptor),
cv.Required(CONF_VALUE): value_schema(),
}
),
)
async def ble_server_descriptor_set_value(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
value = await parse_value(config[CONF_VALUE], args)
cg.add(var.set_buffer(value))
return var
@automation.register_action(
"ble_server.characteristic.notify",
BLECharacteristicNotifyAction,
cv.All(
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(BLECharacteristic),
}
),
validate_notify_action,
),
)
async def ble_server_characteristic_notify(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var

View File

@@ -1,18 +0,0 @@
#include "ble_2901.h"
#include "esphome/components/esp32_ble/ble_uuid.h"
#ifdef USE_ESP32
namespace esphome {
namespace esp32_ble_server {
BLE2901::BLE2901(const std::string &value) : BLE2901((uint8_t *) value.data(), value.length()) {}
BLE2901::BLE2901(const uint8_t *data, size_t length) : BLEDescriptor(esp32_ble::ESPBTUUID::from_uint16(0x2901)) {
this->set_value(data, length);
this->permissions_ = ESP_GATT_PERM_READ;
}
} // namespace esp32_ble_server
} // namespace esphome
#endif

View File

@@ -1,19 +0,0 @@
#pragma once
#include "ble_descriptor.h"
#ifdef USE_ESP32
namespace esphome {
namespace esp32_ble_server {
class BLE2901 : public BLEDescriptor {
public:
BLE2901(const std::string &value);
BLE2901(const uint8_t *data, size_t length);
};
} // namespace esp32_ble_server
} // namespace esphome
#endif

View File

@@ -32,70 +32,36 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
this->set_write_no_response_property((properties & PROPERTY_WRITE_NR) != 0); this->set_write_no_response_property((properties & PROPERTY_WRITE_NR) != 0);
} }
void BLECharacteristic::set_value(std::vector<uint8_t> value) { void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void BLECharacteristic::set_value(const std::vector<uint8_t> &buffer) {
xSemaphoreTake(this->set_value_lock_, 0L); xSemaphoreTake(this->set_value_lock_, 0L);
this->value_ = std::move(value); this->value_ = buffer;
xSemaphoreGive(this->set_value_lock_); xSemaphoreGive(this->set_value_lock_);
} }
void BLECharacteristic::set_value(const std::string &value) { void BLECharacteristic::set_value(const std::string &buffer) {
this->set_value(std::vector<uint8_t>(value.begin(), value.end())); this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end()));
}
void BLECharacteristic::set_value(const uint8_t *data, size_t length) {
this->set_value(std::vector<uint8_t>(data, data + length));
}
void BLECharacteristic::set_value(uint8_t &data) {
uint8_t temp[1];
temp[0] = data;
this->set_value(temp, 1);
}
void BLECharacteristic::set_value(uint16_t &data) {
uint8_t temp[2];
temp[0] = data;
temp[1] = data >> 8;
this->set_value(temp, 2);
}
void BLECharacteristic::set_value(uint32_t &data) {
uint8_t temp[4];
temp[0] = data;
temp[1] = data >> 8;
temp[2] = data >> 16;
temp[3] = data >> 24;
this->set_value(temp, 4);
}
void BLECharacteristic::set_value(int &data) {
uint8_t temp[4];
temp[0] = data;
temp[1] = data >> 8;
temp[2] = data >> 16;
temp[3] = data >> 24;
this->set_value(temp, 4);
}
void BLECharacteristic::set_value(float &data) {
float temp = data;
this->set_value((uint8_t *) &temp, 4);
}
void BLECharacteristic::set_value(double &data) {
double temp = data;
this->set_value((uint8_t *) &temp, 8);
}
void BLECharacteristic::set_value(bool &data) {
uint8_t temp[1];
temp[0] = data;
this->set_value(temp, 1);
} }
void BLECharacteristic::notify(bool notification) { void BLECharacteristic::notify() {
if (!notification) { if (this->service_ == nullptr || this->service_->get_server() == nullptr ||
ESP_LOGW(TAG, "notification=false is not yet supported"); this->service_->get_server()->get_connected_client_count() == 0)
// TODO: Handle when notification=false
}
if (this->service_->get_server()->get_connected_client_count() == 0)
return; return;
for (auto &client : this->service_->get_server()->get_clients()) { for (auto &client : this->service_->get_server()->get_clients()) {
size_t length = this->value_.size(); size_t length = this->value_.size();
esp_err_t err = esp_ble_gatts_send_indicate(this->service_->get_server()->get_gatts_if(), client.first, // If the client is not in the list of clients to notify, skip it
this->handle_, length, this->value_.data(), false); if (this->clients_to_notify_.count(client) == 0)
continue;
// If the client is in the list of clients to notify, check if it requires an ack (i.e. INDICATE)
bool require_ack = this->clients_to_notify_[client];
// TODO: Remove this block when INDICATE acknowledgment is supported
if (require_ack) {
ESP_LOGW(TAG, "INDICATE acknowledgment is not yet supported (i.e. it works as a NOTIFY)");
require_ack = false;
}
esp_err_t err = esp_ble_gatts_send_indicate(this->service_->get_server()->get_gatts_if(), client, this->handle_,
length, this->value_.data(), require_ack);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_send_indicate failed %d", err); ESP_LOGE(TAG, "esp_ble_gatts_send_indicate failed %d", err);
return; return;
@@ -103,7 +69,24 @@ void BLECharacteristic::notify(bool notification) {
} }
} }
void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { this->descriptors_.push_back(descriptor); } void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) {
// If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified
if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) {
descriptor->on(BLEDescriptorEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &value, uint16_t conn_id) {
if (value.size() != 2)
return;
uint16_t cccd = encode_uint16(value[1], value[0]);
bool notify = (cccd & 1) != 0;
bool indicate = (cccd & 2) != 0;
if (notify || indicate) {
this->clients_to_notify_[conn_id] = indicate;
} else {
this->clients_to_notify_.erase(conn_id);
}
});
}
this->descriptors_.push_back(descriptor);
}
void BLECharacteristic::remove_descriptor(BLEDescriptor *descriptor) { void BLECharacteristic::remove_descriptor(BLEDescriptor *descriptor) {
this->descriptors_.erase(std::remove(this->descriptors_.begin(), this->descriptors_.end(), descriptor), this->descriptors_.erase(std::remove(this->descriptors_.begin(), this->descriptors_.end(), descriptor),
@@ -223,6 +206,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
if (!param->read.need_rsp) if (!param->read.need_rsp)
break; // For some reason you can request a read but not want a response break; // For some reason you can request a read but not want a response
this->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::emit_(BLECharacteristicEvt::EmptyEvt::ON_READ,
param->read.conn_id);
uint16_t max_offset = 22; uint16_t max_offset = 22;
esp_gatt_rsp_t response; esp_gatt_rsp_t response;
@@ -262,13 +248,13 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
} }
case ESP_GATTS_WRITE_EVT: { case ESP_GATTS_WRITE_EVT: {
if (this->handle_ != param->write.handle) if (this->handle_ != param->write.handle)
return; break;
if (param->write.is_prep) { if (param->write.is_prep) {
this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len);
this->write_event_ = true; this->write_event_ = true;
} else { } else {
this->set_value(param->write.value, param->write.len); this->set_value(ByteBuffer::wrap(param->write.value, param->write.len));
} }
if (param->write.need_rsp) { if (param->write.need_rsp) {
@@ -289,7 +275,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
} }
if (!param->write.is_prep) { if (!param->write.is_prep) {
this->on_write_(this->value_); this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_(
BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->write.conn_id);
} }
break; break;
@@ -300,7 +287,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
break; break;
this->write_event_ = false; this->write_event_ = false;
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
this->on_write_(this->value_); this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_(
BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->exec_write.conn_id);
} }
esp_err_t err = esp_err_t err =
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr);

View File

@@ -2,8 +2,11 @@
#include "ble_descriptor.h" #include "ble_descriptor.h"
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
#include <vector> #include <vector>
#include <unordered_map>
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -19,24 +22,30 @@ namespace esphome {
namespace esp32_ble_server { namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer;
using namespace event_emitter;
class BLEService; class BLEService;
class BLECharacteristic { namespace BLECharacteristicEvt {
enum VectorEvt {
ON_WRITE,
};
enum EmptyEvt {
ON_READ,
};
} // namespace BLECharacteristicEvt
class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>,
public EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t> {
public: public:
BLECharacteristic(ESPBTUUID uuid, uint32_t properties); BLECharacteristic(ESPBTUUID uuid, uint32_t properties);
~BLECharacteristic(); ~BLECharacteristic();
void set_value(const uint8_t *data, size_t length); void set_value(ByteBuffer buffer);
void set_value(std::vector<uint8_t> value); void set_value(const std::vector<uint8_t> &buffer);
void set_value(const std::string &value); void set_value(const std::string &buffer);
void set_value(uint8_t &data);
void set_value(uint16_t &data);
void set_value(uint32_t &data);
void set_value(int &data);
void set_value(float &data);
void set_value(double &data);
void set_value(bool &data);
void set_broadcast_property(bool value); void set_broadcast_property(bool value);
void set_indicate_property(bool value); void set_indicate_property(bool value);
@@ -45,13 +54,12 @@ class BLECharacteristic {
void set_write_property(bool value); void set_write_property(bool value);
void set_write_no_response_property(bool value); void set_write_no_response_property(bool value);
void notify(bool notification = true); void notify();
void do_create(BLEService *service); void do_create(BLEService *service);
void do_delete() { this->clients_to_notify_.clear(); }
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
void on_write(const std::function<void(const std::vector<uint8_t> &)> &&func) { this->on_write_ = func; }
void add_descriptor(BLEDescriptor *descriptor); void add_descriptor(BLEDescriptor *descriptor);
void remove_descriptor(BLEDescriptor *descriptor); void remove_descriptor(BLEDescriptor *descriptor);
@@ -71,7 +79,7 @@ class BLECharacteristic {
protected: protected:
bool write_event_{false}; bool write_event_{false};
BLEService *service_; BLEService *service_{};
ESPBTUUID uuid_; ESPBTUUID uuid_;
esp_gatt_char_prop_t properties_; esp_gatt_char_prop_t properties_;
uint16_t handle_{0xFFFF}; uint16_t handle_{0xFFFF};
@@ -81,8 +89,7 @@ class BLECharacteristic {
SemaphoreHandle_t set_value_lock_; SemaphoreHandle_t set_value_lock_;
std::vector<BLEDescriptor *> descriptors_; std::vector<BLEDescriptor *> descriptors_;
std::unordered_map<uint16_t, bool> clients_to_notify_;
std::function<void(const std::vector<uint8_t> &)> on_write_;
esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE;

View File

@@ -12,11 +12,19 @@ namespace esp32_ble_server {
static const char *const TAG = "esp32_ble_server.descriptor"; static const char *const TAG = "esp32_ble_server.descriptor";
BLEDescriptor::BLEDescriptor(ESPBTUUID uuid, uint16_t max_len) { static RAMAllocator<uint8_t> descriptor_allocator{}; // NOLINT
BLEDescriptor::BLEDescriptor(ESPBTUUID uuid, uint16_t max_len, bool read, bool write) {
this->uuid_ = uuid; this->uuid_ = uuid;
this->value_.attr_len = 0; this->value_.attr_len = 0;
this->value_.attr_max_len = max_len; this->value_.attr_max_len = max_len;
this->value_.attr_value = (uint8_t *) malloc(max_len); // NOLINT this->value_.attr_value = descriptor_allocator.allocate(max_len);
if (read) {
this->permissions_ |= ESP_GATT_PERM_READ;
}
if (write) {
this->permissions_ |= ESP_GATT_PERM_WRITE;
}
} }
BLEDescriptor::~BLEDescriptor() { free(this->value_.attr_value); } // NOLINT BLEDescriptor::~BLEDescriptor() { free(this->value_.attr_value); } // NOLINT
@@ -38,14 +46,15 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
this->state_ = CREATING; this->state_ = CREATING;
} }
void BLEDescriptor::set_value(const std::string &value) { this->set_value((uint8_t *) value.data(), value.length()); } void BLEDescriptor::set_value(std::vector<uint8_t> buffer) {
void BLEDescriptor::set_value(const uint8_t *data, size_t length) { size_t length = buffer.size();
if (length > this->value_.attr_max_len) { if (length > this->value_.attr_max_len) {
ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len); ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len);
return; return;
} }
this->value_.attr_len = length; this->value_.attr_len = length;
memcpy(this->value_.attr_value, data, length); memcpy(this->value_.attr_value, buffer.data(), length);
} }
void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
@@ -61,10 +70,13 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
break; break;
} }
case ESP_GATTS_WRITE_EVT: { case ESP_GATTS_WRITE_EVT: {
if (this->handle_ == param->write.handle) { if (this->handle_ != param->write.handle)
break;
this->value_.attr_len = param->write.len; this->value_.attr_len = param->write.len;
memcpy(this->value_.attr_value, param->write.value, param->write.len); memcpy(this->value_.attr_value, param->write.value, param->write.len);
} this->emit_(BLEDescriptorEvt::VectorEvt::ON_WRITE,
std::vector<uint8_t>(param->write.value, param->write.value + param->write.len),
param->write.conn_id);
break; break;
} }
default: default:

View File

@@ -1,6 +1,8 @@
#pragma once #pragma once
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -11,17 +13,26 @@ namespace esphome {
namespace esp32_ble_server { namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer;
using namespace event_emitter;
class BLECharacteristic; class BLECharacteristic;
class BLEDescriptor { namespace BLEDescriptorEvt {
enum VectorEvt {
ON_WRITE,
};
} // namespace BLEDescriptorEvt
class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vector<uint8_t>, uint16_t> {
public: public:
BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100); BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true);
virtual ~BLEDescriptor(); virtual ~BLEDescriptor();
void do_create(BLECharacteristic *characteristic); void do_create(BLECharacteristic *characteristic);
ESPBTUUID get_uuid() const { return this->uuid_; }
void set_value(const std::string &value); void set_value(std::vector<uint8_t> buffer);
void set_value(const uint8_t *data, size_t length); void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
@@ -33,9 +44,9 @@ class BLEDescriptor {
ESPBTUUID uuid_; ESPBTUUID uuid_;
uint16_t handle_{0xFFFF}; uint16_t handle_{0xFFFF};
esp_attr_value_t value_; esp_attr_value_t value_{};
esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; esp_gatt_perm_t permissions_{};
enum State : uint8_t { enum State : uint8_t {
FAILED = 0x00, FAILED = 0x00,

View File

@@ -19,11 +19,6 @@ namespace esp32_ble_server {
static const char *const TAG = "esp32_ble_server"; static const char *const TAG = "esp32_ble_server";
static const uint16_t DEVICE_INFORMATION_SERVICE_UUID = 0x180A;
static const uint16_t MODEL_UUID = 0x2A24;
static const uint16_t VERSION_UUID = 0x2A26;
static const uint16_t MANUFACTURER_UUID = 0x2A29;
void BLEServer::setup() { void BLEServer::setup() {
if (this->parent_->is_failed()) { if (this->parent_->is_failed()) {
this->mark_failed(); this->mark_failed();
@@ -38,9 +33,27 @@ void BLEServer::loop() {
return; return;
} }
switch (this->state_) { switch (this->state_) {
case RUNNING: case RUNNING: {
return; // Start all services that are pending to start
if (!this->services_to_start_.empty()) {
uint16_t index_to_remove = 0;
// Iterate over the services to start
for (unsigned i = 0; i < this->services_to_start_.size(); i++) {
BLEService *service = this->services_to_start_[i];
if (service->is_created()) {
service->start(); // Needs to be called once per characteristic in the service
} else {
index_to_remove = i + 1;
}
}
// Remove the services that have been started
if (index_to_remove > 0) {
this->services_to_start_.erase(this->services_to_start_.begin(),
this->services_to_start_.begin() + index_to_remove - 1);
}
}
break;
}
case INIT: { case INIT: {
esp_err_t err = esp_ble_gatts_app_register(0); esp_err_t err = esp_ble_gatts_app_register(0);
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -53,29 +66,26 @@ void BLEServer::loop() {
} }
case REGISTERING: { case REGISTERING: {
if (this->registered_) { if (this->registered_) {
// Create the device information service first so
// it is at the top of the GATT table
this->device_information_service_->do_create(this);
// Create all services previously created // Create all services previously created
for (auto &pair : this->services_) { for (auto &pair : this->services_) {
pair.second->do_create(this); if (pair.second == this->device_information_service_) {
continue;
} }
if (this->device_information_service_ == nullptr) { pair.second->do_create(this);
this->create_service(ESPBTUUID::from_uint16(DEVICE_INFORMATION_SERVICE_UUID));
this->device_information_service_ =
this->get_service(ESPBTUUID::from_uint16(DEVICE_INFORMATION_SERVICE_UUID));
this->create_device_characteristics_();
} }
this->state_ = STARTING_SERVICE; this->state_ = STARTING_SERVICE;
} }
break; break;
} }
case STARTING_SERVICE: { case STARTING_SERVICE: {
if (!this->device_information_service_->is_created()) {
break;
}
if (this->device_information_service_->is_running()) { if (this->device_information_service_->is_running()) {
this->state_ = RUNNING; this->state_ = RUNNING;
this->restart_advertising_(); this->restart_advertising_();
ESP_LOGD(TAG, "BLE server setup successfully"); ESP_LOGD(TAG, "BLE server setup successfully");
} else if (!this->device_information_service_->is_starting()) { } else if (this->device_information_service_->is_created()) {
this->device_information_service_->start(); this->device_information_service_->start();
} }
break; break;
@@ -93,81 +103,66 @@ void BLEServer::restart_advertising_() {
} }
} }
bool BLEServer::create_device_characteristics_() { BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) {
if (this->model_.has_value()) {
BLECharacteristic *model =
this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ);
model->set_value(this->model_.value());
} else {
BLECharacteristic *model =
this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ);
model->set_value(ESPHOME_BOARD);
}
BLECharacteristic *version =
this->device_information_service_->create_characteristic(VERSION_UUID, BLECharacteristic::PROPERTY_READ);
version->set_value("ESPHome " ESPHOME_VERSION);
BLECharacteristic *manufacturer =
this->device_information_service_->create_characteristic(MANUFACTURER_UUID, BLECharacteristic::PROPERTY_READ);
manufacturer->set_value(this->manufacturer_);
return true;
}
void BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles, uint8_t inst_id) {
ESP_LOGV(TAG, "Creating BLE service - %s", uuid.to_string().c_str()); ESP_LOGV(TAG, "Creating BLE service - %s", uuid.to_string().c_str());
// If the service already exists, do nothing // Calculate the inst_id for the service
BLEService *service = this->get_service(uuid); uint8_t inst_id = 0;
if (service != nullptr) { for (; inst_id < 0xFF; inst_id++) {
ESP_LOGW(TAG, "BLE service %s already exists", uuid.to_string().c_str()); if (this->get_service(uuid, inst_id) == nullptr) {
return; break;
} }
service = new BLEService(uuid, num_handles, inst_id, advertise); // NOLINT(cppcoreguidelines-owning-memory) }
this->services_.emplace(uuid.to_string(), service); if (inst_id == 0xFF) {
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", uuid.to_string().c_str());
return nullptr;
}
BLEService *service = // NOLINT(cppcoreguidelines-owning-memory)
new BLEService(uuid, num_handles, inst_id, advertise);
this->services_.emplace(BLEServer::get_service_key(uuid, inst_id), service);
if (this->parent_->is_active() && this->registered_) {
service->do_create(this); service->do_create(this);
}
return service;
} }
void BLEServer::remove_service(ESPBTUUID uuid) { void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
ESP_LOGV(TAG, "Removing BLE service - %s", uuid.to_string().c_str()); ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid.to_string().c_str(), inst_id);
BLEService *service = this->get_service(uuid); BLEService *service = this->get_service(uuid, inst_id);
if (service == nullptr) { if (service == nullptr) {
ESP_LOGW(TAG, "BLE service %s not found", uuid.to_string().c_str()); ESP_LOGW(TAG, "BLE service %s %d does not exist", uuid.to_string().c_str(), inst_id);
return; return;
} }
service->do_delete(); service->do_delete();
delete service; // NOLINT(cppcoreguidelines-owning-memory) delete service; // NOLINT(cppcoreguidelines-owning-memory)
this->services_.erase(uuid.to_string()); this->services_.erase(BLEServer::get_service_key(uuid, inst_id));
} }
BLEService *BLEServer::get_service(ESPBTUUID uuid) { BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) {
BLEService *service = nullptr; BLEService *service = nullptr;
if (this->services_.count(uuid.to_string()) > 0) { if (this->services_.count(BLEServer::get_service_key(uuid, inst_id)) > 0) {
service = this->services_.at(uuid.to_string()); service = this->services_.at(BLEServer::get_service_key(uuid, inst_id));
} }
return service; return service;
} }
std::string BLEServer::get_service_key(ESPBTUUID uuid, uint8_t inst_id) {
return uuid.to_string() + std::to_string(inst_id);
}
void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) { esp_ble_gatts_cb_param_t *param) {
switch (event) { switch (event) {
case ESP_GATTS_CONNECT_EVT: { case ESP_GATTS_CONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client connected"); ESP_LOGD(TAG, "BLE Client connected");
this->add_client_(param->connect.conn_id, (void *) this); this->add_client_(param->connect.conn_id);
this->connected_clients_++; this->emit_(BLEServerEvt::EmptyEvt::ON_CONNECT, param->connect.conn_id);
for (auto *component : this->service_components_) {
component->on_client_connect();
}
break; break;
} }
case ESP_GATTS_DISCONNECT_EVT: { case ESP_GATTS_DISCONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client disconnected"); ESP_LOGD(TAG, "BLE Client disconnected");
if (this->remove_client_(param->disconnect.conn_id)) this->remove_client_(param->disconnect.conn_id);
this->connected_clients_--;
this->parent_->advertising_start(); this->parent_->advertising_start();
for (auto *component : this->service_components_) { this->emit_(BLEServerEvt::EmptyEvt::ON_DISCONNECT, param->disconnect.conn_id);
component->on_client_disconnect();
}
break; break;
} }
case ESP_GATTS_REG_EVT: { case ESP_GATTS_REG_EVT: {

View File

@@ -4,36 +4,38 @@
#include "ble_characteristic.h" #include "ble_characteristic.h"
#include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/esp32_ble/ble_advertising.h"
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/esp32_ble/queue.h" #include "esphome/components/bytebuffer/bytebuffer.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_gap_ble_api.h>
#include <esp_gatts_api.h> #include <esp_gatts_api.h>
namespace esphome { namespace esphome {
namespace esp32_ble_server { namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer;
class BLEServiceComponent { namespace BLEServerEvt {
public: enum EmptyEvt {
virtual void on_client_connect(){}; ON_CONNECT,
virtual void on_client_disconnect(){}; ON_DISCONNECT,
virtual void start();
virtual void stop();
}; };
} // namespace BLEServerEvt
class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented<ESP32BLE> { class BLEServer : public Component,
public GATTsEventHandler,
public BLEStatusEventHandler,
public Parented<ESP32BLE>,
public EventEmitter<BLEServerEvt::EmptyEvt, uint16_t> {
public: public:
void setup() override; void setup() override;
void loop() override; void loop() override;
@@ -44,47 +46,41 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
void teardown(); void teardown();
bool is_running(); bool is_running();
void set_manufacturer(const std::string &manufacturer) { this->manufacturer_ = manufacturer; }
void set_model(const std::string &model) { this->model_ = model; }
void set_manufacturer_data(const std::vector<uint8_t> &data) { void set_manufacturer_data(const std::vector<uint8_t> &data) {
this->manufacturer_data_ = data; this->manufacturer_data_ = data;
this->restart_advertising_(); this->restart_advertising_();
} }
void create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15, uint8_t inst_id = 0); BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15);
void remove_service(ESPBTUUID uuid); void remove_service(ESPBTUUID uuid, uint8_t inst_id = 0);
BLEService *get_service(ESPBTUUID uuid); BLEService *get_service(ESPBTUUID uuid, uint8_t inst_id = 0);
void enqueue_start_service(BLEService *service) { this->services_to_start_.push_back(service); }
void set_device_information_service(BLEService *service) { this->device_information_service_ = service; }
esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } esp_gatt_if_t get_gatts_if() { return this->gatts_if_; }
uint32_t get_connected_client_count() { return this->connected_clients_; } uint32_t get_connected_client_count() { return this->clients_.size(); }
const std::unordered_map<uint16_t, void *> &get_clients() { return this->clients_; } const std::unordered_set<uint16_t> &get_clients() { return this->clients_; }
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) override; esp_ble_gatts_cb_param_t *param) override;
void ble_before_disabled_event_handler() override; void ble_before_disabled_event_handler() override;
void register_service_component(BLEServiceComponent *component) { this->service_components_.push_back(component); }
protected: protected:
bool create_device_characteristics_(); static std::string get_service_key(ESPBTUUID uuid, uint8_t inst_id);
void restart_advertising_(); void restart_advertising_();
void add_client_(uint16_t conn_id, void *client) { this->clients_.emplace(conn_id, client); } void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); }
bool remove_client_(uint16_t conn_id) { return this->clients_.erase(conn_id) > 0; } void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); }
std::string manufacturer_; std::vector<uint8_t> manufacturer_data_{};
optional<std::string> model_;
std::vector<uint8_t> manufacturer_data_;
esp_gatt_if_t gatts_if_{0}; esp_gatt_if_t gatts_if_{0};
bool registered_{false}; bool registered_{false};
uint32_t connected_clients_{0}; std::unordered_set<uint16_t> clients_;
std::unordered_map<uint16_t, void *> clients_; std::unordered_map<std::string, BLEService *> services_{};
std::unordered_map<std::string, BLEService *> services_; std::vector<BLEService *> services_to_start_{};
BLEService *device_information_service_; BLEService *device_information_service_{};
std::vector<BLEServiceComponent *> service_components_;
enum State : uint8_t { enum State : uint8_t {
INIT = 0x00, INIT = 0x00,

View File

@@ -0,0 +1,77 @@
#include "ble_server_automations.h"
#ifdef USE_ESP32
namespace esphome {
namespace esp32_ble_server {
// Interface to interact with ESPHome automations and triggers
namespace esp32_ble_server_automations {
using namespace esp32_ble;
Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_write_trigger(
BLECharacteristic *characteristic) {
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on(
BLECharacteristicEvt::VectorEvt::ON_WRITE,
[on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); });
return on_write_trigger;
}
Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) {
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on(
BLEDescriptorEvt::VectorEvt::ON_WRITE,
[on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); });
return on_write_trigger;
}
Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *server) {
Trigger<uint16_t> *on_connect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory)
server->on(BLEServerEvt::EmptyEvt::ON_CONNECT,
[on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); });
return on_connect_trigger;
}
Trigger<uint16_t> *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) {
Trigger<uint16_t> *on_disconnect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory)
server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT,
[on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); });
return on_disconnect_trigger;
}
void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic,
EventEmitterListenerID listener_id,
const std::function<void()> &pre_notify_listener) {
// Check if there is already a listener for this characteristic
if (this->listeners_.count(characteristic) > 0) {
// Unpack the pair listener_id, pre_notify_listener_id
auto listener_pairs = this->listeners_[characteristic];
EventEmitterListenerID old_listener_id = listener_pairs.first;
EventEmitterListenerID old_pre_notify_listener_id = listener_pairs.second;
// Remove the previous listener
characteristic->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::off(BLECharacteristicEvt::EmptyEvt::ON_READ,
old_listener_id);
// Remove the pre-notify listener
this->off(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, old_pre_notify_listener_id);
}
// Create a new listener for the pre-notify event
EventEmitterListenerID pre_notify_listener_id =
this->on(BLECharacteristicSetValueActionEvt::PRE_NOTIFY,
[pre_notify_listener, characteristic](const BLECharacteristic *evt_characteristic) {
// Only call the pre-notify listener if the characteristic is the one we are interested in
if (characteristic == evt_characteristic) {
pre_notify_listener();
}
});
// Save the pair listener_id, pre_notify_listener_id to the map
this->listeners_[characteristic] = std::make_pair(listener_id, pre_notify_listener_id);
}
} // namespace esp32_ble_server_automations
} // namespace esp32_ble_server
} // namespace esphome
#endif

View File

@@ -0,0 +1,115 @@
#pragma once
#include "ble_server.h"
#include "ble_characteristic.h"
#include "ble_descriptor.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/core/automation.h"
#include <vector>
#include <unordered_map>
#include <functional>
#ifdef USE_ESP32
namespace esphome {
namespace esp32_ble_server {
// Interface to interact with ESPHome actions and triggers
namespace esp32_ble_server_automations {
using namespace esp32_ble;
using namespace event_emitter;
class BLETriggers {
public:
static Trigger<std::vector<uint8_t>, uint16_t> *create_characteristic_on_write_trigger(
BLECharacteristic *characteristic);
static Trigger<std::vector<uint8_t>, uint16_t> *create_descriptor_on_write_trigger(BLEDescriptor *descriptor);
static Trigger<uint16_t> *create_server_on_connect_trigger(BLEServer *server);
static Trigger<uint16_t> *create_server_on_disconnect_trigger(BLEServer *server);
};
enum BLECharacteristicSetValueActionEvt {
PRE_NOTIFY,
};
// Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic
class BLECharacteristicSetValueActionManager
: public EventEmitter<BLECharacteristicSetValueActionEvt, BLECharacteristic *> {
public:
// Singleton pattern
static BLECharacteristicSetValueActionManager *get_instance() {
static BLECharacteristicSetValueActionManager instance;
return &instance;
}
void set_listener(BLECharacteristic *characteristic, EventEmitterListenerID listener_id,
const std::function<void()> &pre_notify_listener);
EventEmitterListenerID get_listener(BLECharacteristic *characteristic) {
return this->listeners_[characteristic].first;
}
void emit_pre_notify(BLECharacteristic *characteristic) {
this->emit_(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, characteristic);
}
private:
std::unordered_map<BLECharacteristic *, std::pair<EventEmitterListenerID, EventEmitterListenerID>> listeners_;
};
template<typename... Ts> class BLECharacteristicSetValueAction : public Action<Ts...> {
public:
BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(Ts... x) override {
// If the listener is already set, do nothing
if (BLECharacteristicSetValueActionManager::get_instance()->get_listener(this->parent_) == this->listener_id_)
return;
// Set initial value
this->parent_->set_value(this->buffer_.value(x...));
// Set the listener for read events
this->listener_id_ = this->parent_->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::on(
BLECharacteristicEvt::EmptyEvt::ON_READ, [this, x...](uint16_t id) {
// Set the value of the characteristic every time it is read
this->parent_->set_value(this->buffer_.value(x...));
});
// Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic
BLECharacteristicSetValueActionManager::get_instance()->set_listener(
this->parent_, this->listener_id_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); });
}
protected:
BLECharacteristic *parent_;
EventEmitterListenerID listener_id_;
};
template<typename... Ts> class BLECharacteristicNotifyAction : public Action<Ts...> {
public:
BLECharacteristicNotifyAction(BLECharacteristic *characteristic) : parent_(characteristic) {}
void play(Ts... x) override {
// Call the pre-notify event
BLECharacteristicSetValueActionManager::get_instance()->emit_pre_notify(this->parent_);
// Notify the characteristic
this->parent_->notify();
}
protected:
BLECharacteristic *parent_;
};
template<typename... Ts> class BLEDescriptorSetValueAction : public Action<Ts...> {
public:
BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(Ts... x) override { this->parent_->set_value(this->buffer_.value(x...)); }
protected:
BLEDescriptor *parent_;
};
} // namespace esp32_ble_server_automations
} // namespace esp32_ble_server
} // namespace esphome
#endif

View File

@@ -52,18 +52,21 @@ void BLEService::do_create(BLEServer *server) {
esp_err_t err = esp_ble_gatts_create_service(server->get_gatts_if(), &srvc_id, this->num_handles_); esp_err_t err = esp_ble_gatts_create_service(server->get_gatts_if(), &srvc_id, this->num_handles_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_create_service failed: %d", err); ESP_LOGE(TAG, "esp_ble_gatts_create_service failed: %d", err);
this->init_state_ = FAILED; this->state_ = FAILED;
return; return;
} }
this->init_state_ = CREATING; this->state_ = CREATING;
} }
void BLEService::do_delete() { void BLEService::do_delete() {
if (this->init_state_ == DELETING || this->init_state_ == DELETED) if (this->state_ == DELETING || this->state_ == DELETED)
return; return;
this->init_state_ = DELETING; this->state_ = DELETING;
this->created_characteristic_count_ = 0; this->created_characteristic_count_ = 0;
this->last_created_characteristic_ = nullptr; this->last_created_characteristic_ = nullptr;
// Call all characteristics to delete
for (auto *characteristic : this->characteristics_)
characteristic->do_delete();
this->stop_(); this->stop_();
esp_err_t err = esp_ble_gatts_delete_service(this->handle_); esp_err_t err = esp_ble_gatts_delete_service(this->handle_);
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -91,6 +94,7 @@ void BLEService::start() {
return; return;
should_start_ = true; should_start_ = true;
this->state_ = STARTING;
esp_err_t err = esp_ble_gatts_start_service(this->handle_); esp_err_t err = esp_ble_gatts_start_service(this->handle_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_start_service failed: %d", err); ESP_LOGE(TAG, "esp_ble_gatts_start_service failed: %d", err);
@@ -98,7 +102,6 @@ void BLEService::start() {
} }
if (this->advertise_) if (this->advertise_)
esp32_ble::global_ble->advertising_add_service_uuid(this->uuid_); esp32_ble::global_ble->advertising_add_service_uuid(this->uuid_);
this->running_state_ = STARTING;
} }
void BLEService::stop() { void BLEService::stop() {
@@ -107,9 +110,9 @@ void BLEService::stop() {
} }
void BLEService::stop_() { void BLEService::stop_() {
if (this->running_state_ == STOPPING || this->running_state_ == STOPPED) if (this->state_ == STOPPING || this->state_ == STOPPED)
return; return;
this->running_state_ = STOPPING; this->state_ = STOPPING;
esp_err_t err = esp_ble_gatts_stop_service(this->handle_); esp_err_t err = esp_ble_gatts_stop_service(this->handle_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_stop_service failed: %d", err); ESP_LOGE(TAG, "esp_ble_gatts_stop_service failed: %d", err);
@@ -119,17 +122,16 @@ void BLEService::stop_() {
esp32_ble::global_ble->advertising_remove_service_uuid(this->uuid_); esp32_ble::global_ble->advertising_remove_service_uuid(this->uuid_);
} }
bool BLEService::is_created() { return this->init_state_ == CREATED; }
bool BLEService::is_failed() { bool BLEService::is_failed() {
if (this->init_state_ == FAILED) if (this->state_ == FAILED)
return true; return true;
bool failed = false; bool failed = false;
for (auto *characteristic : this->characteristics_) for (auto *characteristic : this->characteristics_)
failed |= characteristic->is_failed(); failed |= characteristic->is_failed();
if (failed) if (failed)
this->init_state_ = FAILED; this->state_ = FAILED;
return this->init_state_ == FAILED; return this->state_ == FAILED;
} }
void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
@@ -139,7 +141,7 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g
if (this->uuid_ == ESPBTUUID::from_uuid(param->create.service_id.id.uuid) && if (this->uuid_ == ESPBTUUID::from_uuid(param->create.service_id.id.uuid) &&
this->inst_id_ == param->create.service_id.id.inst_id) { this->inst_id_ == param->create.service_id.id.inst_id) {
this->handle_ = param->create.service_handle; this->handle_ = param->create.service_handle;
this->init_state_ = CREATED; this->state_ = CREATED;
if (this->should_start_) if (this->should_start_)
this->start(); this->start();
} }
@@ -147,18 +149,18 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g
} }
case ESP_GATTS_DELETE_EVT: case ESP_GATTS_DELETE_EVT:
if (param->del.service_handle == this->handle_) { if (param->del.service_handle == this->handle_) {
this->init_state_ = DELETED; this->state_ = DELETED;
} }
break; break;
case ESP_GATTS_START_EVT: { case ESP_GATTS_START_EVT: {
if (param->start.service_handle == this->handle_) { if (param->start.service_handle == this->handle_) {
this->running_state_ = RUNNING; this->state_ = RUNNING;
} }
break; break;
} }
case ESP_GATTS_STOP_EVT: { case ESP_GATTS_STOP_EVT: {
if (param->start.service_handle == this->handle_) { if (param->start.service_handle == this->handle_) {
this->running_state_ = STOPPED; this->state_ = STOPPED;
} }
break; break;
} }

View File

@@ -32,6 +32,7 @@ class BLEService {
BLECharacteristic *create_characteristic(ESPBTUUID uuid, esp_gatt_char_prop_t properties); BLECharacteristic *create_characteristic(ESPBTUUID uuid, esp_gatt_char_prop_t properties);
ESPBTUUID get_uuid() { return this->uuid_; } ESPBTUUID get_uuid() { return this->uuid_; }
uint8_t get_inst_id() { return this->inst_id_; }
BLECharacteristic *get_last_created_characteristic() { return this->last_created_characteristic_; } BLECharacteristic *get_last_created_characteristic() { return this->last_created_characteristic_; }
uint16_t get_handle() { return this->handle_; } uint16_t get_handle() { return this->handle_; }
@@ -44,18 +45,17 @@ class BLEService {
void start(); void start();
void stop(); void stop();
bool is_created();
bool is_failed(); bool is_failed();
bool is_created() { return this->state_ == CREATED; }
bool is_running() { return this->running_state_ == RUNNING; } bool is_running() { return this->state_ == RUNNING; }
bool is_starting() { return this->running_state_ == STARTING; } bool is_starting() { return this->state_ == STARTING; }
bool is_deleted() { return this->init_state_ == DELETED; } bool is_deleted() { return this->state_ == DELETED; }
protected: protected:
std::vector<BLECharacteristic *> characteristics_; std::vector<BLECharacteristic *> characteristics_;
BLECharacteristic *last_created_characteristic_{nullptr}; BLECharacteristic *last_created_characteristic_{nullptr};
uint32_t created_characteristic_count_{0}; uint32_t created_characteristic_count_{0};
BLEServer *server_; BLEServer *server_ = nullptr;
ESPBTUUID uuid_; ESPBTUUID uuid_;
uint16_t num_handles_; uint16_t num_handles_;
uint16_t handle_{0xFFFF}; uint16_t handle_{0xFFFF};
@@ -66,22 +66,18 @@ class BLEService {
bool do_create_characteristics_(); bool do_create_characteristics_();
void stop_(); void stop_();
enum InitState : uint8_t { enum State : uint8_t {
FAILED = 0x00, FAILED = 0x00,
INIT, INIT,
CREATING, CREATING,
CREATING_DEPENDENTS,
CREATED, CREATED,
DELETING,
DELETED,
} init_state_{INIT};
enum RunningState : uint8_t {
STARTING, STARTING,
RUNNING, RUNNING,
STOPPING, STOPPING,
STOPPED, STOPPED,
} running_state_{STOPPED}; DELETING,
DELETED,
} state_{INIT};
}; };
} // namespace esp32_ble_server } // namespace esp32_ble_server

View File

@@ -1,9 +1,13 @@
import re
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32_ble from esphome.components import esp32_ble
from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import (
bt_uuid,
bt_uuid16_format,
bt_uuid32_format,
bt_uuid128_format,
)
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ACTIVE, CONF_ACTIVE,
@@ -86,43 +90,6 @@ def validate_scan_parameters(config):
return config return config
bt_uuid16_format = "XXXX"
bt_uuid32_format = "XXXXXXXX"
bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
def bt_uuid(value):
in_value = cv.string_strict(value)
value = in_value.upper()
if len(value) == len(bt_uuid16_format):
pattern = re.compile("^[A-F|0-9]{4,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'"
)
return value
if len(value) == len(bt_uuid32_format):
pattern = re.compile("^[A-F|0-9]{8,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'"
)
return value
if len(value) == len(bt_uuid128_format):
pattern = re.compile(
"^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$"
)
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 128 UUID format: '{in_value}'"
)
return value
raise cv.Invalid(
f"Service UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format"
)
def as_hex(value): def as_hex(value):
return cg.RawExpression(f"0x{value}ULL") return cg.RawExpression(f"0x{value}ULL")

View File

@@ -1,6 +1,6 @@
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import binary_sensor, esp32_ble_server, output from esphome.components import binary_sensor, output
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
@@ -24,9 +24,7 @@ Error = improv_ns.enum("Error")
State = improv_ns.enum("State") State = improv_ns.enum("State")
esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv") esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv")
ESP32ImprovComponent = esp32_improv_ns.class_( ESP32ImprovComponent = esp32_improv_ns.class_("ESP32ImprovComponent", cg.Component)
"ESP32ImprovComponent", cg.Component, esp32_ble_server.BLEServiceComponent
)
ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_( ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_(
"ESP32ImprovProvisionedTrigger", automation.Trigger.template() "ESP32ImprovProvisionedTrigger", automation.Trigger.template()
) )
@@ -47,7 +45,6 @@ ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
cv.GenerateID(CONF_BLE_SERVER_ID): cv.use_id(esp32_ble_server.BLEServer),
cv.Required(CONF_AUTHORIZER): cv.Any( cv.Required(CONF_AUTHORIZER): cv.Any(
cv.none, cv.use_id(binary_sensor.BinarySensor) cv.none, cv.use_id(binary_sensor.BinarySensor)
), ),
@@ -100,9 +97,6 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
ble_server = await cg.get_variable(config[CONF_BLE_SERVER_ID])
cg.add(ble_server.register_service_component(var))
cg.add_define("USE_IMPROV") cg.add_define("USE_IMPROV")
cg.add_library("improv/Improv", "1.2.4") cg.add_library("improv/Improv", "1.2.4")

View File

@@ -4,12 +4,15 @@
#include "esphome/components/esp32_ble_server/ble_2902.h" #include "esphome/components/esp32_ble_server/ble_2902.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
namespace esphome { namespace esphome {
namespace esp32_improv { namespace esp32_improv {
using namespace bytebuffer;
static const char *const TAG = "esp32_improv.component"; static const char *const TAG = "esp32_improv.component";
static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
@@ -26,6 +29,8 @@ void ESP32ImprovComponent::setup() {
}); });
} }
#endif #endif
global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT,
[this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
} }
void ESP32ImprovComponent::setup_characteristics() { void ESP32ImprovComponent::setup_characteristics() {
@@ -40,7 +45,8 @@ void ESP32ImprovComponent::setup_characteristics() {
this->error_->add_descriptor(error_descriptor); this->error_->add_descriptor(error_descriptor);
this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE); this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE);
this->rpc_->on_write([this](const std::vector<uint8_t> &data) { this->rpc_->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on(
BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &data, uint16_t id) {
if (!data.empty()) { if (!data.empty()) {
this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end());
} }
@@ -62,7 +68,7 @@ void ESP32ImprovComponent::setup_characteristics() {
if (this->status_indicator_ != nullptr) if (this->status_indicator_ != nullptr)
capabilities |= improv::CAPABILITY_IDENTIFY; capabilities |= improv::CAPABILITY_IDENTIFY;
#endif #endif
this->capabilities_->set_value(capabilities); this->capabilities_->set_value(ByteBuffer::wrap(capabilities));
this->setup_complete_ = true; this->setup_complete_ = true;
} }
@@ -80,8 +86,7 @@ void ESP32ImprovComponent::loop() {
if (this->service_ == nullptr) { if (this->service_ == nullptr) {
// Setup the service // Setup the service
ESP_LOGD(TAG, "Creating Improv service"); ESP_LOGD(TAG, "Creating Improv service");
global_ble_server->create_service(ESPBTUUID::from_raw(improv::SERVICE_UUID), true); this->service_ = global_ble_server->create_service(ESPBTUUID::from_raw(improv::SERVICE_UUID), true);
this->service_ = global_ble_server->get_service(ESPBTUUID::from_raw(improv::SERVICE_UUID));
this->setup_characteristics(); this->setup_characteristics();
} }
@@ -93,15 +98,15 @@ void ESP32ImprovComponent::loop() {
case improv::STATE_STOPPED: case improv::STATE_STOPPED:
this->set_status_indicator_state_(false); this->set_status_indicator_state_(false);
if (this->service_->is_created() && this->should_start_ && this->setup_complete_) { if (this->should_start_ && this->setup_complete_) {
if (this->service_->is_running()) { if (this->service_->is_created()) {
this->service_->start();
} else if (this->service_->is_running()) {
esp32_ble::global_ble->advertising_start(); esp32_ble::global_ble->advertising_start();
this->set_state_(improv::STATE_AWAITING_AUTHORIZATION); this->set_state_(improv::STATE_AWAITING_AUTHORIZATION);
this->set_error_(improv::ERROR_NONE); this->set_error_(improv::ERROR_NONE);
ESP_LOGD(TAG, "Service started!"); ESP_LOGD(TAG, "Service started!");
} else {
this->service_->start();
} }
} }
break; break;
@@ -199,8 +204,7 @@ void ESP32ImprovComponent::set_state_(improv::State state) {
ESP_LOGV(TAG, "Setting state: %d", state); ESP_LOGV(TAG, "Setting state: %d", state);
this->state_ = state; this->state_ = state;
if (this->status_->get_value().empty() || this->status_->get_value()[0] != state) { if (this->status_->get_value().empty() || this->status_->get_value()[0] != state) {
uint8_t data[1]{state}; this->status_->set_value(ByteBuffer::wrap(static_cast<uint8_t>(state)));
this->status_->set_value(data, 1);
if (state != improv::STATE_STOPPED) if (state != improv::STATE_STOPPED)
this->status_->notify(); this->status_->notify();
} }
@@ -232,15 +236,14 @@ void ESP32ImprovComponent::set_error_(improv::Error error) {
ESP_LOGE(TAG, "Error: %d", error); ESP_LOGE(TAG, "Error: %d", error);
} }
if (this->error_->get_value().empty() || this->error_->get_value()[0] != error) { if (this->error_->get_value().empty() || this->error_->get_value()[0] != error) {
uint8_t data[1]{error}; this->error_->set_value(ByteBuffer::wrap(static_cast<uint8_t>(error)));
this->error_->set_value(data, 1);
if (this->state_ != improv::STATE_STOPPED) if (this->state_ != improv::STATE_STOPPED)
this->error_->notify(); this->error_->notify();
} }
} }
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) { void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) {
this->rpc_response_->set_value(response); this->rpc_response_->set_value(ByteBuffer::wrap(response));
if (this->state_ != improv::STATE_STOPPED) if (this->state_ != improv::STATE_STOPPED)
this->rpc_response_->notify(); this->rpc_response_->notify();
} }
@@ -339,8 +342,6 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() {
wifi::global_wifi_component->clear_sta(); wifi::global_wifi_component->clear_sta();
} }
void ESP32ImprovComponent::on_client_disconnect() { this->set_error_(improv::ERROR_NONE); };
ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esp32_improv } // namespace esp32_improv

View File

@@ -32,18 +32,17 @@ namespace esp32_improv {
using namespace esp32_ble_server; using namespace esp32_ble_server;
class ESP32ImprovComponent : public Component, public BLEServiceComponent { class ESP32ImprovComponent : public Component {
public: public:
ESP32ImprovComponent(); ESP32ImprovComponent();
void dump_config() override; void dump_config() override;
void loop() override; void loop() override;
void setup() override; void setup() override;
void setup_characteristics(); void setup_characteristics();
void on_client_disconnect() override;
float get_setup_priority() const override; float get_setup_priority() const override;
void start() override; void start();
void stop() override; void stop();
bool is_active() const { return this->state_ != improv::STATE_STOPPED; } bool is_active() const { return this->state_ != improv::STATE_STOPPED; }
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK #ifdef USE_ESP32_IMPROV_STATE_CALLBACK

View File

@@ -127,12 +127,12 @@ CONFIG_SCHEMA = cv.All(
), ),
OptionalForIDF5( OptionalForIDF5(
CONF_RMT_SYMBOLS, CONF_RMT_SYMBOLS,
esp32_idf=64, esp32_idf=192,
esp32_s2_idf=64, esp32_s2_idf=192,
esp32_s3_idf=48, esp32_s3_idf=192,
esp32_c3_idf=48, esp32_c3_idf=96,
esp32_c6_idf=48, esp32_c6_idf=96,
esp32_h2_idf=48, esp32_h2_idf=96,
): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)), ): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)),
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),

View File

@@ -34,6 +34,7 @@ from .gpio import PinInitialState, add_pin_initial_states_array
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["preferences"] AUTO_LOAD = ["preferences"]
IS_TARGET_PLATFORM = True
def set_core_data(config): def set_core_data(config):

View File

@@ -0,0 +1,5 @@
CODEOWNERS = ["@Rapsssito"]
# Allows event_emitter to be configured in yaml, to allow use of the C++ api.
CONFIG_SCHEMA = {}

View File

@@ -0,0 +1,14 @@
#include "event_emitter.h"
namespace esphome {
namespace event_emitter {
static const char *const TAG = "event_emitter";
void raise_event_emitter_full_error() {
ESP_LOGE(TAG, "EventEmitter has reached the maximum number of listeners for event");
ESP_LOGW(TAG, "Removing listener to make space for new listener");
}
} // namespace event_emitter
} // namespace esphome

View File

@@ -0,0 +1,63 @@
#pragma once
#include <unordered_map>
#include <vector>
#include <functional>
#include <limits>
#include "esphome/core/log.h"
namespace esphome {
namespace event_emitter {
using EventEmitterListenerID = uint32_t;
void raise_event_emitter_full_error();
// EventEmitter class that can emit events with a specific name (it is highly recommended to use an enum class for this)
// and a list of arguments. Supports multiple listeners for each event.
template<typename EvtType, typename... Args> class EventEmitter {
public:
EventEmitterListenerID on(EvtType event, std::function<void(Args...)> listener) {
EventEmitterListenerID listener_id = get_next_id_(event);
listeners_[event][listener_id] = listener;
return listener_id;
}
void off(EvtType event, EventEmitterListenerID id) {
if (listeners_.count(event) == 0)
return;
listeners_[event].erase(id);
}
protected:
void emit_(EvtType event, Args... args) {
if (listeners_.count(event) == 0)
return;
for (const auto &listener : listeners_[event]) {
listener.second(args...);
}
}
EventEmitterListenerID get_next_id_(EvtType event) {
// Check if the map is full
if (listeners_[event].size() == std::numeric_limits<EventEmitterListenerID>::max()) {
// Raise an error if the map is full
raise_event_emitter_full_error();
off(event, 0);
return 0;
}
// Get the next ID for the given event.
EventEmitterListenerID next_id = (current_id_ + 1) % std::numeric_limits<EventEmitterListenerID>::max();
while (listeners_[event].count(next_id) > 0) {
next_id = (next_id + 1) % std::numeric_limits<EventEmitterListenerID>::max();
}
current_id_ = next_id;
return current_id_;
}
private:
std::unordered_map<EvtType, std::unordered_map<EventEmitterListenerID, std::function<void(Args...)>>> listeners_;
EventEmitterListenerID current_id_ = 0;
};
} // namespace event_emitter
} // namespace esphome

View File

@@ -17,6 +17,7 @@ from .gpio import host_pin_to_code # noqa
CODEOWNERS = ["@esphome/core", "@clydebarrow"] CODEOWNERS = ["@esphome/core", "@clydebarrow"]
AUTO_LOAD = ["network", "preferences"] AUTO_LOAD = ["network", "preferences"]
IS_TARGET_PLATFORM = True
def set_core_data(config): def set_core_data(config):

View File

@@ -273,11 +273,9 @@ IMAGE_TYPE = {
"GRAYSCALE": ImageGrayscale, "GRAYSCALE": ImageGrayscale,
"RGB565": ImageRGB565, "RGB565": ImageRGB565,
"RGB": ImageRGB, "RGB": ImageRGB,
"TRANSPARENT_BINARY": ReplaceWith( "TRANSPARENT_BINARY": ReplaceWith("'type: BINARY' and 'transparency: chroma_key'"),
"'type: BINARY' and 'use_transparency: chroma_key'"
),
"RGB24": ReplaceWith("'type: RGB'"), "RGB24": ReplaceWith("'type: RGB'"),
"RGBA": ReplaceWith("'type: RGB' and 'use_transparency: alpha_channel'"), "RGBA": ReplaceWith("'type: RGB' and 'transparency: alpha_channel'"),
} }
TransparencyType = image_ns.enum("TransparencyType") TransparencyType = image_ns.enum("TransparencyType")

View File

@@ -47,6 +47,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@kuba2k2"] CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["preferences"] AUTO_LOAD = ["preferences"]
IS_TARGET_PLATFORM = True
def _detect_variant(value): def _detect_variant(value):

View File

@@ -186,6 +186,8 @@ CONFIG_SCHEMA = cv.All(
esp32_s3_idf=USB_SERIAL_JTAG, esp32_s3_idf=USB_SERIAL_JTAG,
esp32_c3_arduino=USB_CDC, esp32_c3_arduino=USB_CDC,
esp32_c3_idf=USB_SERIAL_JTAG, esp32_c3_idf=USB_SERIAL_JTAG,
esp32_c6_arduino=USB_CDC,
esp32_c6_idf=USB_SERIAL_JTAG,
rp2040=USB_CDC, rp2040=USB_CDC,
bk72xx=DEFAULT, bk72xx=DEFAULT,
rtl87xx=DEFAULT, rtl87xx=DEFAULT,

View File

@@ -91,7 +91,7 @@ async def to_code(config):
add_idf_component( add_idf_component(
name="mdns", name="mdns",
repo="https://github.com/espressif/esp-protocols.git", repo="https://github.com/espressif/esp-protocols.git",
ref="mdns-v1.3.2", ref="mdns-v1.5.1",
path="components/mdns", path="components/mdns",
) )

View File

@@ -72,9 +72,9 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// max_temp // max_temp
root[MQTT_MAX_TEMP] = traits.get_visual_max_temperature(); root[MQTT_MAX_TEMP] = traits.get_visual_max_temperature();
// target_temp_step // target_temp_step
root[MQTT_TARGET_TEMPERATURE_STEP] = traits.get_visual_target_temperature_step(); root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1;
// current_temp_step // current_temp_step
root[MQTT_CURRENT_TEMPERATURE_STEP] = traits.get_visual_current_temperature_step(); root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1;
// temperature units are always coerced to Celsius internally // temperature units are always coerced to Celsius internally
root[MQTT_TEMPERATURE_UNIT] = "C"; root[MQTT_TEMPERATURE_UNIT] = "C";

View File

@@ -49,6 +49,7 @@ struct IPAddress {
} }
IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); }
IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; }
std::string str() const { return str_lower_case(inet_ntoa(ip_addr_)); }
#else #else
IPAddress() { ip_addr_set_zero(&ip_addr_); } IPAddress() { ip_addr_set_zero(&ip_addr_); }
IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) {
@@ -119,6 +120,7 @@ struct IPAddress {
bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr) bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr)
bool is_ip4() { return IP_IS_V4(&ip_addr_); } bool is_ip4() { return IP_IS_V4(&ip_addr_); }
bool is_ip6() { return IP_IS_V6(&ip_addr_); } bool is_ip6() { return IP_IS_V6(&ip_addr_); }
bool is_multicast() { return ip_addr_ismulticast(&ip_addr_); }
std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); } std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); }
bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); } bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); } bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); }

View File

@@ -52,6 +52,23 @@ class Format:
pass pass
class BMPFormat(Format):
def __init__(self):
super().__init__("BMP")
def actions(self):
cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT")
class JPEGFormat(Format):
def __init__(self):
super().__init__("JPEG")
def actions(self):
cg.add_define("USE_ONLINE_IMAGE_JPEG_SUPPORT")
cg.add_library("JPEGDEC", "1.6.2", "https://github.com/bitbank2/JPEGDEC")
class PNGFormat(Format): class PNGFormat(Format):
def __init__(self): def __init__(self):
super().__init__("PNG") super().__init__("PNG")
@@ -61,8 +78,15 @@ class PNGFormat(Format):
cg.add_library("pngle", "1.0.2") cg.add_library("pngle", "1.0.2")
# New formats can be added here. IMAGE_FORMATS = {
IMAGE_FORMATS = {x.image_type: x for x in (PNGFormat(),)} x.image_type: x
for x in (
BMPFormat(),
JPEGFormat(),
PNGFormat(),
)
}
IMAGE_FORMATS.update({"JPG": IMAGE_FORMATS["JPEG"]})
OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_)
@@ -102,7 +126,7 @@ ONLINE_IMAGE_SCHEMA = (
cv.Required(CONF_URL): cv.url, cv.Required(CONF_URL): cv.url,
cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True),
cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_),
cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536), cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(

View File

@@ -0,0 +1,101 @@
#include "bmp_image.h"
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
#include "esphome/components/display/display.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace online_image {
static const char *const TAG = "online_image.bmp";
int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
size_t index = 0;
if (this->current_index_ == 0 && index == 0 && size > 14) {
/**
* BMP file format:
* 0-1: Signature (BM)
* 2-5: File size
* 6-9: Reserved
* 10-13: Pixel data offset
*
* Integer values are stored in little-endian format.
*/
// Check if the file is a BMP image
if (buffer[0] != 'B' || buffer[1] != 'M') {
ESP_LOGE(TAG, "Not a BMP file");
return DECODE_ERROR_INVALID_TYPE;
}
this->download_size_ = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]);
this->data_offset_ = encode_uint32(buffer[13], buffer[12], buffer[11], buffer[10]);
this->current_index_ = 14;
index = 14;
}
if (this->current_index_ == 14 && index == 14 && size > this->data_offset_) {
/**
* BMP DIB header:
* 14-17: DIB header size
* 18-21: Image width
* 22-25: Image height
* 26-27: Number of color planes
* 28-29: Bits per pixel
* 30-33: Compression method
* 34-37: Image data size
* 38-41: Horizontal resolution
* 42-45: Vertical resolution
* 46-49: Number of colors in the color table
*/
this->width_ = encode_uint32(buffer[21], buffer[20], buffer[19], buffer[18]);
this->height_ = encode_uint32(buffer[25], buffer[24], buffer[23], buffer[22]);
this->bits_per_pixel_ = encode_uint16(buffer[29], buffer[28]);
this->compression_method_ = encode_uint32(buffer[33], buffer[32], buffer[31], buffer[30]);
this->image_data_size_ = encode_uint32(buffer[37], buffer[36], buffer[35], buffer[34]);
this->color_table_entries_ = encode_uint32(buffer[49], buffer[48], buffer[47], buffer[46]);
switch (this->bits_per_pixel_) {
case 1:
this->width_bytes_ = (this->width_ % 8 == 0) ? (this->width_ / 8) : (this->width_ / 8 + 1);
break;
default:
ESP_LOGE(TAG, "Unsupported bits per pixel: %d", this->bits_per_pixel_);
return DECODE_ERROR_UNSUPPORTED_FORMAT;
}
if (this->compression_method_ != 0) {
ESP_LOGE(TAG, "Unsupported compression method: %d", this->compression_method_);
return DECODE_ERROR_UNSUPPORTED_FORMAT;
}
if (!this->set_size(this->width_, this->height_)) {
return DECODE_ERROR_OUT_OF_MEMORY;
}
this->current_index_ = this->data_offset_;
index = this->data_offset_;
}
while (index < size) {
size_t paint_index = this->current_index_ - this->data_offset_;
uint8_t current_byte = buffer[index];
for (uint8_t i = 0; i < 8; i++) {
size_t x = (paint_index * 8) % this->width_ + i;
size_t y = (this->height_ - 1) - (paint_index / this->width_bytes_);
Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF;
this->draw(x, y, 1, 1, c);
}
this->current_index_++;
index++;
}
this->decoded_bytes_ += size;
return size;
};
} // namespace online_image
} // namespace esphome
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT

View File

@@ -0,0 +1,40 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
#include "image_decoder.h"
namespace esphome {
namespace online_image {
/**
* @brief Image decoder specialization for PNG images.
*/
class BmpDecoder : public ImageDecoder {
public:
/**
* @brief Construct a new BMP Decoder object.
*
* @param display The image to decode the stream into.
*/
BmpDecoder(OnlineImage *image) : ImageDecoder(image) {}
int HOT decode(uint8_t *buffer, size_t size) override;
protected:
size_t current_index_{0};
ssize_t width_{0};
ssize_t height_{0};
uint16_t bits_per_pixel_{0};
uint32_t compression_method_{0};
uint32_t image_data_size_{0};
uint32_t color_table_entries_{0};
size_t width_bytes_{0};
size_t data_offset_{0};
};
} // namespace online_image
} // namespace esphome
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT

View File

@@ -8,10 +8,11 @@ namespace online_image {
static const char *const TAG = "online_image.decoder"; static const char *const TAG = "online_image.decoder";
void ImageDecoder::set_size(int width, int height) { bool ImageDecoder::set_size(int width, int height) {
this->image_->resize_(width, height); bool resized = this->image_->resize_(width, height);
this->x_scale_ = static_cast<double>(this->image_->buffer_width_) / width; this->x_scale_ = static_cast<double>(this->image_->buffer_width_) / width;
this->y_scale_ = static_cast<double>(this->image_->buffer_height_) / height; this->y_scale_ = static_cast<double>(this->image_->buffer_height_) / height;
return resized;
} }
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
@@ -40,5 +41,20 @@ size_t DownloadBuffer::read(size_t len) {
return this->unread_; return this->unread_;
} }
size_t DownloadBuffer::resize(size_t size) {
if (this->size_ == size) {
return size;
}
this->allocator_.deallocate(this->buffer_, this->size_);
this->size_ = size;
this->buffer_ = this->allocator_.allocate(size);
this->reset();
if (this->buffer_) {
return size;
} else {
return 0;
}
}
} // namespace online_image } // namespace online_image
} // namespace esphome } // namespace esphome

View File

@@ -4,6 +4,12 @@
namespace esphome { namespace esphome {
namespace online_image { namespace online_image {
enum DecodeError : int {
DECODE_ERROR_INVALID_TYPE = -1,
DECODE_ERROR_UNSUPPORTED_FORMAT = -2,
DECODE_ERROR_OUT_OF_MEMORY = -3,
};
class OnlineImage; class OnlineImage;
/** /**
@@ -24,7 +30,7 @@ class ImageDecoder {
* *
* @param download_size The total number of bytes that need to be downloaded for the image. * @param download_size The total number of bytes that need to be downloaded for the image.
*/ */
virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; } virtual void prepare(size_t download_size) { this->download_size_ = download_size; }
/** /**
* @brief Decode a part of the image. It will try reading from the buffer. * @brief Decode a part of the image. It will try reading from the buffer.
@@ -45,8 +51,9 @@ class ImageDecoder {
* *
* @param width The image's width. * @param width The image's width.
* @param height The image's height. * @param height The image's height.
* @return true if the image was resized, false otherwise.
*/ */
void set_size(int width, int height); bool set_size(int width, int height);
/** /**
* @brief Fill a rectangle on the display_buffer using the defined color. * @brief Fill a rectangle on the display_buffer using the defined color.
@@ -68,8 +75,8 @@ class ImageDecoder {
OnlineImage *image_; OnlineImage *image_;
// Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_". // Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_".
// Will be overwritten anyway once the download size is known. // Will be overwritten anyway once the download size is known.
uint32_t download_size_ = 1; size_t download_size_ = 1;
uint32_t decoded_bytes_ = 0; size_t decoded_bytes_ = 0;
double x_scale_ = 1.0; double x_scale_ = 1.0;
double y_scale_ = 1.0; double y_scale_ = 1.0;
}; };
@@ -99,6 +106,8 @@ class DownloadBuffer {
void reset() { this->unread_ = 0; } void reset() { this->unread_ = 0; }
size_t resize(size_t size);
protected: protected:
RAMAllocator<uint8_t> allocator_{}; RAMAllocator<uint8_t> allocator_{};
uint8_t *buffer_; uint8_t *buffer_;

View File

@@ -0,0 +1,89 @@
#include "jpeg_image.h"
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
#include "esphome/components/display/display_buffer.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "online_image.h"
static const char *const TAG = "online_image.jpeg";
namespace esphome {
namespace online_image {
/**
* @brief Callback method that will be called by the JPEGDEC engine when a chunk
* of the image is decoded.
*
* @param jpeg The JPEGDRAW object, including the context data.
*/
static int draw_callback(JPEGDRAW *jpeg) {
ImageDecoder *decoder = (ImageDecoder *) jpeg->pUser;
// Some very big images take too long to decode, so feed the watchdog on each callback
// to avoid crashing.
App.feed_wdt();
size_t position = 0;
for (size_t y = 0; y < jpeg->iHeight; y++) {
for (size_t x = 0; x < jpeg->iWidth; x++) {
auto rg = decode_value(jpeg->pPixels[position++]);
auto ba = decode_value(jpeg->pPixels[position++]);
Color color(rg[1], rg[0], ba[1], ba[0]);
if (!decoder) {
ESP_LOGE(TAG, "Decoder pointer is null!");
return 0;
}
decoder->draw(jpeg->x + x, jpeg->y + y, 1, 1, color);
}
}
return 1;
}
void JpegDecoder::prepare(size_t download_size) {
ImageDecoder::prepare(download_size);
auto size = this->image_->resize_download_buffer(download_size);
if (size < download_size) {
ESP_LOGE(TAG, "Resize failed!");
// TODO: return an error code;
}
}
int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
if (size < this->download_size_) {
ESP_LOGV(TAG, "Download not complete. Size: %d/%d", size, this->download_size_);
return 0;
}
if (!this->jpeg_.openRAM(buffer, size, draw_callback)) {
ESP_LOGE(TAG, "Could not open image for decoding.");
return DECODE_ERROR_INVALID_TYPE;
}
auto jpeg_type = this->jpeg_.getJPEGType();
if (jpeg_type == JPEG_MODE_INVALID) {
ESP_LOGE(TAG, "Unsupported JPEG image");
return DECODE_ERROR_INVALID_TYPE;
} else if (jpeg_type == JPEG_MODE_PROGRESSIVE) {
ESP_LOGE(TAG, "Progressive JPEG images not supported");
return DECODE_ERROR_INVALID_TYPE;
}
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
this->jpeg_.setUserPointer(this);
this->jpeg_.setPixelType(RGB8888);
this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight());
if (!this->jpeg_.decode(0, 0, 0)) {
ESP_LOGE(TAG, "Error while decoding.");
this->jpeg_.close();
return DECODE_ERROR_UNSUPPORTED_FORMAT;
}
this->decoded_bytes_ = size;
this->jpeg_.close();
return size;
}
} // namespace online_image
} // namespace esphome
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT

View File

@@ -0,0 +1,34 @@
#pragma once
#include "image_decoder.h"
#include "esphome/core/defines.h"
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
#include <JPEGDEC.h>
namespace esphome {
namespace online_image {
/**
* @brief Image decoder specialization for JPEG images.
*/
class JpegDecoder : public ImageDecoder {
public:
/**
* @brief Construct a new JPEG Decoder object.
*
* @param display The image to decode the stream into.
*/
JpegDecoder(OnlineImage *image) : ImageDecoder(image) {}
~JpegDecoder() override {}
void prepare(size_t download_size) override;
int HOT decode(uint8_t *buffer, size_t size) override;
protected:
JPEGDEC jpeg_{};
};
} // namespace online_image
} // namespace esphome
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT

View File

@@ -6,6 +6,12 @@ static const char *const TAG = "online_image";
#include "image_decoder.h" #include "image_decoder.h"
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
#include "bmp_image.h"
#endif
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
#include "jpeg_image.h"
#endif
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include "png_image.h" #include "png_image.h"
#endif #endif
@@ -29,6 +35,7 @@ OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFor
: Image(nullptr, 0, 0, type, transparency), : Image(nullptr, 0, 0, type, transparency),
buffer_(nullptr), buffer_(nullptr),
download_buffer_(download_buffer_size), download_buffer_(download_buffer_size),
download_buffer_initial_size_(download_buffer_size),
format_(format), format_(format),
fixed_width_(width), fixed_width_(width),
fixed_height_(height) { fixed_height_(height) {
@@ -118,20 +125,34 @@ void OnlineImage::update() {
ESP_LOGD(TAG, "Starting download"); ESP_LOGD(TAG, "Starting download");
size_t total_size = this->downloader_->content_length; size_t total_size = this->downloader_->content_length;
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
if (this->format_ == ImageFormat::BMP) {
ESP_LOGD(TAG, "Allocating BMP decoder");
this->decoder_ = make_unique<BmpDecoder>(this);
}
#endif // ONLINE_IMAGE_BMP_SUPPORT
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
if (this->format_ == ImageFormat::JPEG) {
ESP_LOGD(TAG, "Allocating JPEG decoder");
this->decoder_ = esphome::make_unique<JpegDecoder>(this);
}
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
if (this->format_ == ImageFormat::PNG) { if (this->format_ == ImageFormat::PNG) {
this->decoder_ = esphome::make_unique<PngDecoder>(this); ESP_LOGD(TAG, "Allocating PNG decoder");
this->decoder_ = make_unique<PngDecoder>(this);
} }
#endif // ONLINE_IMAGE_PNG_SUPPORT #endif // ONLINE_IMAGE_PNG_SUPPORT
if (!this->decoder_) { if (!this->decoder_) {
ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported."); ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_);
this->end_connection_(); this->end_connection_();
this->download_error_callback_.call(); this->download_error_callback_.call();
return; return;
} }
this->decoder_->prepare(total_size); this->decoder_->prepare(total_size);
ESP_LOGI(TAG, "Downloading image"); ESP_LOGI(TAG, "Downloading image (Size: %d)", total_size);
this->start_time_ = ::time(nullptr);
} }
void OnlineImage::loop() { void OnlineImage::loop() {
@@ -145,6 +166,7 @@ void OnlineImage::loop() {
this->height_ = buffer_height_; this->height_ = buffer_height_;
ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(), ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(),
this->width_, this->height_); this->width_, this->height_);
ESP_LOGD(TAG, "Total time: %lds", ::time(nullptr) - this->start_time_);
this->end_connection_(); this->end_connection_();
this->download_finished_callback_.call(); this->download_finished_callback_.call();
return; return;
@@ -155,6 +177,10 @@ void OnlineImage::loop() {
} }
size_t available = this->download_buffer_.free_capacity(); size_t available = this->download_buffer_.free_capacity();
if (available) { if (available) {
// Some decoders need to fully download the image before downloading.
// In case of huge images, don't wait blocking until the whole image has been downloaded,
// use smaller chunks
available = std::min(available, this->download_buffer_initial_size_);
auto len = this->downloader_->read(this->download_buffer_.append(), available); auto len = this->downloader_->read(this->download_buffer_.append(), available);
if (len > 0) { if (len > 0) {
this->download_buffer_.write(len); this->download_buffer_.write(len);

View File

@@ -1,10 +1,10 @@
#pragma once #pragma once
#include "esphome/components/http_request/http_request.h"
#include "esphome/components/image/image.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/components/http_request/http_request.h"
#include "esphome/components/image/image.h"
#include "image_decoder.h" #include "image_decoder.h"
@@ -23,10 +23,12 @@ using t_http_codes = enum {
enum ImageFormat { enum ImageFormat {
/** Automatically detect from MIME type. Not supported yet. */ /** Automatically detect from MIME type. Not supported yet. */
AUTO, AUTO,
/** JPEG format. Not supported yet. */ /** JPEG format. */
JPEG, JPEG,
/** PNG format. */ /** PNG format. */
PNG, PNG,
/** BMP format. */
BMP,
}; };
/** /**
@@ -77,6 +79,13 @@ class OnlineImage : public PollingComponent,
*/ */
void release(); void release();
/**
* Resize the download buffer
*
* @param size The new size for the download buffer.
*/
size_t resize_download_buffer(size_t size) { return this->download_buffer_.resize(size); }
void add_on_finished_callback(std::function<void()> &&callback); void add_on_finished_callback(std::function<void()> &&callback);
void add_on_error_callback(std::function<void()> &&callback); void add_on_error_callback(std::function<void()> &&callback);
@@ -117,6 +126,12 @@ class OnlineImage : public PollingComponent,
uint8_t *buffer_; uint8_t *buffer_;
DownloadBuffer download_buffer_; DownloadBuffer download_buffer_;
/**
* This is the *initial* size of the download buffer, not the current size.
* The download buffer can be resized at runtime; the download_buffer_initial_size_
* will *not* change even if the download buffer has been resized.
*/
size_t download_buffer_initial_size_;
const ImageFormat format_; const ImageFormat format_;
image::Image *placeholder_{nullptr}; image::Image *placeholder_{nullptr};
@@ -146,7 +161,9 @@ class OnlineImage : public PollingComponent,
*/ */
int buffer_height_; int buffer_height_;
friend void ImageDecoder::set_size(int width, int height); time_t start_time_;
friend bool ImageDecoder::set_size(int width, int height);
friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color); friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color);
}; };

View File

@@ -2,7 +2,6 @@
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include "esphome/components/display/display_buffer.h" #include "esphome/components/display/display_buffer.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -41,7 +40,7 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui
decoder->draw(x, y, w, h, color); decoder->draw(x, y, w, h, color);
} }
void PngDecoder::prepare(uint32_t download_size) { void PngDecoder::prepare(size_t download_size) {
ImageDecoder::prepare(download_size); ImageDecoder::prepare(download_size);
pngle_set_user_data(this->pngle_, this); pngle_set_user_data(this->pngle_, this);
pngle_set_init_callback(this->pngle_, init_callback); pngle_set_init_callback(this->pngle_, init_callback);
@@ -51,7 +50,7 @@ void PngDecoder::prepare(uint32_t download_size) {
int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
if (!this->pngle_) { if (!this->pngle_) {
ESP_LOGE(TAG, "PNG decoder engine not initialized!"); ESP_LOGE(TAG, "PNG decoder engine not initialized!");
return -1; return DECODE_ERROR_OUT_OF_MEMORY;
} }
if (size < 256 && size < this->download_size_ - this->decoded_bytes_) { if (size < 256 && size < this->download_size_ - this->decoded_bytes_) {
ESP_LOGD(TAG, "Waiting for data"); ESP_LOGD(TAG, "Waiting for data");

View File

@@ -21,7 +21,7 @@ class PngDecoder : public ImageDecoder {
PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {} PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {}
~PngDecoder() override { pngle_destroy(this->pngle_); } ~PngDecoder() override { pngle_destroy(this->pngle_); }
void prepare(uint32_t download_size) override; void prepare(size_t download_size) override;
int HOT decode(uint8_t *buffer, size_t size) override; int HOT decode(uint8_t *buffer, size_t size) override;
protected: protected:

View File

@@ -75,6 +75,8 @@ void PulseMeterSensor::loop() {
case MeterState::RUNNING: { case MeterState::RUNNING: {
uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_; uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_;
float pulse_width_us = delta_us / float(this->get_->count_); float pulse_width_us = delta_us / float(this->get_->count_);
ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us,
this->get_->count_, pulse_width_us);
this->publish_state((60.0f * 1000000.0f) / pulse_width_us); this->publish_state((60.0f * 1000000.0f) / pulse_width_us);
} break; } break;
} }

View File

@@ -1,6 +1,6 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, esp32_rmt, remote_base from esphome.components import esp32_rmt, remote_base
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BUFFER_SIZE, CONF_BUFFER_SIZE,
@@ -158,9 +158,6 @@ async def to_code(config):
cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION]))
if CONF_FILTER_SYMBOLS in config: if CONF_FILTER_SYMBOLS in config:
cg.add(var.set_filter_symbols(config[CONF_FILTER_SYMBOLS])) cg.add(var.set_filter_symbols(config[CONF_FILTER_SYMBOLS]))
if CORE.using_esp_idf:
esp32.add_idf_sdkconfig_option("CONFIG_RMT_RECV_FUNC_IN_IRAM", True)
esp32.add_idf_sdkconfig_option("CONFIG_RMT_ISR_IRAM_SAFE", True)
else: else:
if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None: if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None:
var = cg.new_Pvariable( var = cg.new_Pvariable(

View File

@@ -37,7 +37,7 @@ void RemoteTransmitterComponent::await_target_time_() {
const uint32_t current_time = micros(); const uint32_t current_time = micros();
if (this->target_time_ == 0) { if (this->target_time_ == 0) {
this->target_time_ = current_time; this->target_time_ = current_time;
} else if (this->target_time_ > current_time) { } else if ((int32_t) (this->target_time_ - current_time) > 0) {
delayMicroseconds(this->target_time_ - current_time); delayMicroseconds(this->target_time_ - current_time);
} }
} }
@@ -50,13 +50,13 @@ void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint
if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) {
while (true) { // Modulate with carrier frequency while (true) { // Modulate with carrier frequency
this->target_time_ += on_time; this->target_time_ += on_time;
if (this->target_time_ >= target) if ((int32_t) (this->target_time_ - target) >= 0)
break; break;
this->await_target_time_(); this->await_target_time_();
this->pin_->digital_write(false); this->pin_->digital_write(false);
this->target_time_ += off_time; this->target_time_ += off_time;
if (this->target_time_ >= target) if ((int32_t) (this->target_time_ - target) >= 0)
break; break;
this->await_target_time_(); this->await_target_time_();
this->pin_->digital_write(true); this->pin_->digital_write(true);

View File

@@ -38,7 +38,7 @@ void RemoteTransmitterComponent::await_target_time_() {
if (this->target_time_ == 0) { if (this->target_time_ == 0) {
this->target_time_ = current_time; this->target_time_ = current_time;
} else { } else {
while (this->target_time_ > micros()) { while ((int32_t) (this->target_time_ - micros()) > 0) {
// busy loop that ensures micros is constantly called // busy loop that ensures micros is constantly called
} }
} }
@@ -52,13 +52,13 @@ void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint
if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) {
while (true) { // Modulate with carrier frequency while (true) { // Modulate with carrier frequency
this->target_time_ += on_time; this->target_time_ += on_time;
if (this->target_time_ >= target) if ((int32_t) (this->target_time_ - target) >= 0)
break; break;
this->await_target_time_(); this->await_target_time_();
this->pin_->digital_write(false); this->pin_->digital_write(false);
this->target_time_ += off_time; this->target_time_ += off_time;
if (this->target_time_ >= target) if ((int32_t) (this->target_time_ - target) >= 0)
break; break;
this->await_target_time_(); this->await_target_time_();
this->pin_->digital_write(true); this->pin_->digital_write(true);

View File

@@ -27,6 +27,7 @@ from .gpio import rp2040_pin_to_code # noqa
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
AUTO_LOAD = ["preferences"] AUTO_LOAD = ["preferences"]
IS_TARGET_PLATFORM = True
def set_core_data(config): def set_core_data(config):

View File

@@ -19,6 +19,8 @@ from .boards import RTL87XX_BOARD_PINS, RTL87XX_BOARDS
CODEOWNERS = ["@kuba2k2"] CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["libretiny"] AUTO_LOAD = ["libretiny"]
IS_TARGET_PLATFORM = True
COMPONENT_DATA = LibreTinyComponent( COMPONENT_DATA = LibreTinyComponent(
name=COMPONENT_RTL87XX, name=COMPONENT_RTL87XX,

View File

@@ -88,7 +88,7 @@ void SPIDelegateBitBash::write(uint16_t data, size_t num_bits) { this->transfer_
uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) { uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) {
// Clock starts out at idle level // Clock starts out at idle level
this->clk_pin_->digital_write(clock_polarity_); this->clk_pin_->digital_write(clock_polarity_);
uint8_t out_data = 0; uint16_t out_data = 0;
for (uint8_t i = 0; i != num_bits; i++) { for (uint8_t i = 0; i != num_bits; i++) {
uint8_t shift; uint8_t shift;

View File

@@ -27,6 +27,7 @@ UDPComponent = udp_ns.class_("UDPComponent", cg.PollingComponent)
CONF_BROADCAST = "broadcast" CONF_BROADCAST = "broadcast"
CONF_BROADCAST_ID = "broadcast_id" CONF_BROADCAST_ID = "broadcast_id"
CONF_ADDRESSES = "addresses" CONF_ADDRESSES = "addresses"
CONF_LISTEN_ADDRESS = "listen_address"
CONF_PROVIDER = "provider" CONF_PROVIDER = "provider"
CONF_PROVIDERS = "providers" CONF_PROVIDERS = "providers"
CONF_REMOTE_ID = "remote_id" CONF_REMOTE_ID = "remote_id"
@@ -84,6 +85,9 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(UDPComponent), cv.GenerateID(): cv.declare_id(UDPComponent),
cv.Optional(CONF_PORT, default=18511): cv.port, cv.Optional(CONF_PORT, default=18511): cv.port,
cv.Optional(
CONF_LISTEN_ADDRESS, default="255.255.255.255"
): cv.ipv4address_multi_broadcast,
cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list(
cv.ipv4address, cv.ipv4address,
), ),
@@ -154,5 +158,7 @@ async def to_code(config):
for provider in config.get(CONF_PROVIDERS, ()): for provider in config.get(CONF_PROVIDERS, ()):
name = provider[CONF_NAME] name = provider[CONF_NAME]
cg.add(var.add_provider(name)) cg.add(var.add_provider(name))
if (listen_address := str(config[CONF_LISTEN_ADDRESS])) != "255.255.255.255":
cg.add(var.set_listen_address(listen_address))
if encryption := provider.get(CONF_ENCRYPTION): if encryption := provider.get(CONF_ENCRYPTION):
cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption)))

View File

@@ -249,6 +249,21 @@ void UDPComponent::setup() {
server.sin_addr.s_addr = ESPHOME_INADDR_ANY; server.sin_addr.s_addr = ESPHOME_INADDR_ANY;
server.sin_port = htons(this->port_); server.sin_port = htons(this->port_);
if (this->listen_address_.has_value()) {
struct ip_mreq imreq = {};
imreq.imr_interface.s_addr = ESPHOME_INADDR_ANY;
inet_aton(this->listen_address_.value().str().c_str(), &imreq.imr_multiaddr);
server.sin_addr.s_addr = imreq.imr_multiaddr.s_addr;
ESP_LOGV(TAG, "Join multicast %s", this->listen_address_.value().str().c_str());
err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq));
if (err < 0) {
ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno);
this->mark_failed();
this->status_set_error("Failed to set IP_ADD_MEMBERSHIP");
return;
}
}
err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server));
if (err != 0) { if (err != 0) {
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
@@ -565,6 +580,9 @@ void UDPComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Ping-pong: %s", YESNO(this->ping_pong_enable_)); ESP_LOGCONFIG(TAG, " Ping-pong: %s", YESNO(this->ping_pong_enable_));
for (const auto &address : this->addresses_) for (const auto &address : this->addresses_)
ESP_LOGCONFIG(TAG, " Address: %s", address.c_str()); ESP_LOGCONFIG(TAG, " Address: %s", address.c_str());
if (this->listen_address_.has_value()) {
ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str().c_str());
}
#ifdef USE_SENSOR #ifdef USE_SENSOR
for (auto sensor : this->sensors_) for (auto sensor : this->sensors_)
ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id); ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id);

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/network/ip_address.h"
#ifdef USE_SENSOR #ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#endif #endif
@@ -69,6 +70,7 @@ class UDPComponent : public PollingComponent {
} }
#endif #endif
void add_address(const char *addr) { this->addresses_.emplace_back(addr); } void add_address(const char *addr) { this->addresses_.emplace_back(addr); }
void set_listen_address(const char *listen_addr) { this->listen_address_ = network::IPAddress(listen_addr); }
void set_port(uint16_t port) { this->port_ = port; } void set_port(uint16_t port) { this->port_ = port; }
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
@@ -143,6 +145,7 @@ class UDPComponent : public PollingComponent {
std::map<std::string, std::map<std::string, binary_sensor::BinarySensor *>> remote_binary_sensors_{}; std::map<std::string, std::map<std::string, binary_sensor::BinarySensor *>> remote_binary_sensors_{};
#endif #endif
optional<network::IPAddress> listen_address_{};
std::map<std::string, Provider> providers_{}; std::map<std::string, Provider> providers_{};
std::vector<uint8_t> ping_header_{}; std::vector<uint8_t> ping_header_{};
std::vector<uint8_t> header_{}; std::vector<uint8_t> header_{};

View File

@@ -22,7 +22,6 @@ from esphome.const import (
CONF_PACKAGES, CONF_PACKAGES,
CONF_PLATFORM, CONF_PLATFORM,
CONF_SUBSTITUTIONS, CONF_SUBSTITUTIONS,
TARGET_PLATFORMS,
) )
from esphome.core import CORE, DocumentRange, EsphomeError from esphome.core import CORE, DocumentRange, EsphomeError
import esphome.core.config as core_config import esphome.core.config as core_config
@@ -833,7 +832,7 @@ def validate_config(
result[CONF_ESPHOME] = config[CONF_ESPHOME] result[CONF_ESPHOME] = config[CONF_ESPHOME]
result.add_output_path([CONF_ESPHOME], CONF_ESPHOME) result.add_output_path([CONF_ESPHOME], CONF_ESPHOME)
try: try:
core_config.preload_core_config(config, result) target_platform = core_config.preload_core_config(config, result)
except vol.Invalid as err: except vol.Invalid as err:
result.add_error(err) result.add_error(err)
return result return result
@@ -845,9 +844,9 @@ def validate_config(
cv.All(cv.version_number, cv.validate_esphome_version)(min_version) cv.All(cv.version_number, cv.validate_esphome_version)(min_version)
# First run platform validation steps # First run platform validation steps
for key in TARGET_PLATFORMS: result.add_validation_step(
if key in config: LoadValidationStep(target_platform, config[target_platform])
result.add_validation_step(LoadValidationStep(key, config[key])) )
result.run_validation_steps() result.run_validation_steps()
if result.errors: if result.errors:

View File

@@ -1168,6 +1168,15 @@ def ipv4address(value):
return address return address
def ipv4address_multi_broadcast(value):
address = ipv4address(value)
if not (address.is_multicast or (address == IPv4Address("255.255.255.255"))):
raise Invalid(
f"{value} is not a multicasst address nor local broadcast address"
)
return address
def ipaddress(value): def ipaddress(value):
try: try:
address = ip_address(value) address = ip_address(value)

View File

@@ -15,15 +15,6 @@ PLATFORM_LIBRETINY_OLDSTYLE = "libretiny"
PLATFORM_RP2040 = "rp2040" PLATFORM_RP2040 = "rp2040"
PLATFORM_RTL87XX = "rtl87xx" PLATFORM_RTL87XX = "rtl87xx"
TARGET_PLATFORMS = [
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_HOST,
PLATFORM_LIBRETINY_OLDSTYLE,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
]
SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"}
HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"} HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"}
@@ -539,6 +530,7 @@ CONF_NETWORKS = "networks"
CONF_NEW_PASSWORD = "new_password" CONF_NEW_PASSWORD = "new_password"
CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide" CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide"
CONF_NOISE_LEVEL = "noise_level" CONF_NOISE_LEVEL = "noise_level"
CONF_NOTIFY = "notify"
CONF_NUM_ATTEMPTS = "num_attempts" CONF_NUM_ATTEMPTS = "num_attempts"
CONF_NUM_CHANNELS = "num_channels" CONF_NUM_CHANNELS = "num_channels"
CONF_NUM_CHIPS = "num_chips" CONF_NUM_CHIPS = "num_chips"

View File

@@ -582,7 +582,7 @@ class EsphomeCore:
@property @property
def config_dir(self): def config_dir(self):
return os.path.dirname(self.config_path) return os.path.dirname(os.path.abspath(self.config_path))
@property @property
def data_dir(self): def data_dir(self):

View File

@@ -1,6 +1,7 @@
import logging import logging
import multiprocessing import multiprocessing
import os import os
from pathlib import Path
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
@@ -28,7 +29,6 @@ from esphome.const import (
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_VERSION, CONF_VERSION,
KEY_CORE, KEY_CORE,
TARGET_PLATFORMS,
__version__ as ESPHOME_VERSION, __version__ as ESPHOME_VERSION,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
@@ -174,7 +174,31 @@ PRELOAD_CONFIG_SCHEMA = cv.Schema(
) )
def preload_core_config(config, result): def _is_target_platform(name):
from esphome.loader import get_component
try:
if get_component(name, True).is_target_platform:
return True
except KeyError:
pass
return False
def _list_target_platforms():
target_platforms = []
root = Path(__file__).parents[1]
for path in (root / "components").iterdir():
if not path.is_dir():
continue
if not (path / "__init__.py").is_file():
continue
if _is_target_platform(path.name):
target_platforms += [path.name]
return target_platforms
def preload_core_config(config, result) -> str:
with cv.prepend_path(CONF_ESPHOME): with cv.prepend_path(CONF_ESPHOME):
conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME]) conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME])
@@ -187,12 +211,16 @@ def preload_core_config(config, result):
conf[CONF_BUILD_PATH] = os.path.join(build_path, CORE.name) conf[CONF_BUILD_PATH] = os.path.join(build_path, CORE.name)
CORE.build_path = CORE.relative_internal_path(conf[CONF_BUILD_PATH]) CORE.build_path = CORE.relative_internal_path(conf[CONF_BUILD_PATH])
target_platforms = [key for key in TARGET_PLATFORMS if key in config] target_platforms = []
for domain, _ in config.items():
if _is_target_platform(domain):
target_platforms += [domain]
if not target_platforms: if not target_platforms:
raise cv.Invalid( raise cv.Invalid(
"Platform missing. You must include one of the available platform keys: " "Platform missing. You must include one of the available platform keys: "
+ ", ".join(TARGET_PLATFORMS), + ", ".join(_list_target_platforms()),
[CONF_ESPHOME], [CONF_ESPHOME],
) )
if len(target_platforms) > 1: if len(target_platforms) > 1:
@@ -202,6 +230,7 @@ def preload_core_config(config, result):
) )
config[CONF_ESPHOME] = conf config[CONF_ESPHOME] = conf
return target_platforms[0]
def include_file(path, basename): def include_file(path, basename):

View File

@@ -60,7 +60,9 @@
#define USE_NETWORK #define USE_NETWORK
#define USE_NEXTION_TFT_UPLOAD #define USE_NEXTION_TFT_UPLOAD
#define USE_NUMBER #define USE_NUMBER
#define USE_ONLINE_IMAGE_BMP_SUPPORT
#define USE_ONLINE_IMAGE_PNG_SUPPORT #define USE_ONLINE_IMAGE_PNG_SUPPORT
#define USE_ONLINE_IMAGE_JPEG_SUPPORT
#define USE_OTA #define USE_OTA
#define USE_OTA_PASSWORD #define USE_OTA_PASSWORD
#define USE_OTA_STATE_CALLBACK #define USE_OTA_STATE_CALLBACK

View File

@@ -2,6 +2,8 @@
#include <cassert> #include <cassert>
#include <cstdarg> #include <cstdarg>
// for PRIu32 and friends
#include <cinttypes>
#include <string> #include <string>
#ifdef USE_STORE_LOG_STR_IN_FLASH #ifdef USE_STORE_LOG_STR_IN_FLASH

View File

@@ -7,7 +7,7 @@ dependencies:
version: v2.0.9 version: v2.0.9
mdns: mdns:
git: https://github.com/espressif/esp-protocols.git git: https://github.com/espressif/esp-protocols.git
version: mdns-v1.3.2 version: mdns-v1.5.1
path: components/mdns path: components/mdns
rules: rules:
- if: "idf_version >=5.0" - if: "idf_version >=5.0"

View File

@@ -52,6 +52,10 @@ class ComponentManifest:
def is_platform_component(self) -> bool: def is_platform_component(self) -> bool:
return getattr(self.module, "IS_PLATFORM_COMPONENT", False) return getattr(self.module, "IS_PLATFORM_COMPONENT", False)
@property
def is_target_platform(self) -> bool:
return getattr(self.module, "IS_TARGET_PLATFORM", False)
@property @property
def config_schema(self) -> Optional[Any]: def config_schema(self) -> Optional[Any]:
return getattr(self.module, "CONFIG_SCHEMA", None) return getattr(self.module, "CONFIG_SCHEMA", None)
@@ -169,13 +173,15 @@ def install_custom_components_meta_finder():
install_meta_finder(custom_components_dir) install_meta_finder(custom_components_dir)
def _lookup_module(domain): def _lookup_module(domain, exception):
if domain in _COMPONENT_CACHE: if domain in _COMPONENT_CACHE:
return _COMPONENT_CACHE[domain] return _COMPONENT_CACHE[domain]
try: try:
module = importlib.import_module(f"esphome.components.{domain}") module = importlib.import_module(f"esphome.components.{domain}")
except ImportError as e: except ImportError as e:
if exception:
raise
if "No module named" in str(e): if "No module named" in str(e):
_LOGGER.info( _LOGGER.info(
"Unable to import component %s: %s", domain, str(e), exc_info=False "Unable to import component %s: %s", domain, str(e), exc_info=False
@@ -184,6 +190,8 @@ def _lookup_module(domain):
_LOGGER.error("Unable to import component %s:", domain, exc_info=True) _LOGGER.error("Unable to import component %s:", domain, exc_info=True)
return None return None
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
if exception:
raise
_LOGGER.error("Unable to load component %s:", domain, exc_info=True) _LOGGER.error("Unable to load component %s:", domain, exc_info=True)
return None return None
@@ -192,14 +200,14 @@ def _lookup_module(domain):
return manif return manif
def get_component(domain): def get_component(domain, exception=False):
assert "." not in domain assert "." not in domain
return _lookup_module(domain) return _lookup_module(domain, exception)
def get_platform(domain, platform): def get_platform(domain, platform):
full = f"{platform}.{domain}" full = f"{platform}.{domain}"
return _lookup_module(full) return _lookup_module(full, False)
_COMPONENT_CACHE = {} _COMPONENT_CACHE = {}

View File

@@ -41,6 +41,8 @@ lib_deps =
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
pavlodn/HaierProtocol@0.9.31 ; haier pavlodn/HaierProtocol@0.9.31 ; haier
kikuchan98/pngle@1.0.2 ; online_image kikuchan98/pngle@1.0.2 ; online_image
; Using the repository directly, otherwise ESP-IDF can't use the library
https://github.com/bitbank2/JPEGDEC.git#1.6.2 ; online_image
; This is using the repository until a new release is published to PlatformIO ; This is using the repository until a new release is published to PlatformIO
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
lvgl/lvgl@8.4.0 ; lvgl lvgl/lvgl@8.4.0 ; lvgl

View File

@@ -10,4 +10,5 @@ sensor:
- platform: ads1115 - platform: ads1115
multiplexer: A0_A1 multiplexer: A0_A1
gain: 1.024 gain: 1.024
sample_rate: 128
id: ads1115_sensor id: ads1115_sensor

View File

@@ -10,4 +10,5 @@ sensor:
- platform: ads1115 - platform: ads1115
multiplexer: A0_A1 multiplexer: A0_A1
gain: 1.024 gain: 1.024
sample_rate: 128
id: ads1115_sensor id: ads1115_sensor

View File

@@ -10,4 +10,5 @@ sensor:
- platform: ads1115 - platform: ads1115
multiplexer: A0_A1 multiplexer: A0_A1
gain: 1.024 gain: 1.024
sample_rate: 128
id: ads1115_sensor id: ads1115_sensor

View File

@@ -10,4 +10,5 @@ sensor:
- platform: ads1115 - platform: ads1115
multiplexer: A0_A1 multiplexer: A0_A1
gain: 1.024 gain: 1.024
sample_rate: 128
id: ads1115_sensor id: ads1115_sensor

View File

@@ -10,4 +10,5 @@ sensor:
- platform: ads1115 - platform: ads1115
multiplexer: A0_A1 multiplexer: A0_A1
gain: 1.024 gain: 1.024
sample_rate: 128
id: ads1115_sensor id: ads1115_sensor

View File

@@ -10,4 +10,5 @@ sensor:
- platform: ads1115 - platform: ads1115
multiplexer: A0_A1 multiplexer: A0_A1
gain: 1.024 gain: 1.024
sample_rate: 128
id: ads1115_sensor id: ads1115_sensor

View File

@@ -1,3 +1,66 @@
esp32_ble_server: esp32_ble_server:
id: ble id: ble_server
manufacturer_data: [0x72, 0x4, 0x00, 0x23] manufacturer_data: [0x72, 0x4, 0x00, 0x23]
manufacturer: ESPHome
model: Test
on_connect:
- lambda: |-
ESP_LOGD("BLE", "Connection from %d", id);
on_disconnect:
- lambda: |-
ESP_LOGD("BLE", "Disconnection from %d", id);
services:
- uuid: 2a24b789-7aab-4535-af3e-ee76a35cc12d
advertise: false
characteristics:
- id: test_notify_characteristic
description: "Notify characteristic"
uuid: cad48e28-7fbe-41cf-bae9-d77a6c233423
read: true
notify: true
value: [1, 2, 3, 4]
descriptors:
- uuid: cad48e28-7fbe-41cf-bae9-d77a6c111111
on_write:
logger.log:
format: "Descriptor write id %u, data %s"
args: [id, 'format_hex_pretty(x.data(), x.size()).c_str()']
value:
data: "123.1"
type: float
endianness: BIG
- uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc42d
advertise: false
characteristics:
- id: test_change_characteristic
uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc11c
read: true
value:
data: "Initial"
string_encoding: utf-8
description: Change characteristic
descriptors:
- uuid: 0x4414
id: test_change_descriptor
value: "Initial descriptor value"
- uuid: 0x2312
value:
data: 0x12
type: uint16_t
on_write:
- lambda: |-
ESP_LOGD("BLE", "Descriptor received: %s from %d", std::string(x.begin(), x.end()).c_str(), id);
- uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc99a
write: true
on_write:
then:
- lambda: |-
ESP_LOGD("BLE", "Characteristic received: %s from %d", std::string(x.begin(), x.end()).c_str(), id);
- ble_server.characteristic.set_value:
id: test_change_characteristic
value: !lambda 'return bytebuffer::ByteBuffer::wrap({0x00, 0x01, 0x02}).get_data();'
- ble_server.characteristic.notify:
id: test_notify_characteristic
- ble_server.descriptor.set_value:
id: test_change_descriptor
value: !lambda return bytebuffer::ByteBuffer::wrap({0x03, 0x04, 0x05}).get_data();

View File

@@ -834,12 +834,12 @@ image:
resize: 256x48 resize: 256x48
file: $component_dir/logo-text.svg file: $component_dir/logo-text.svg
type: RGB565 type: RGB565
use_transparency: alpha_channel transparency: alpha_channel
- id: dog_image - id: dog_image
file: $component_dir/logo-text.svg file: $component_dir/logo-text.svg
resize: 256x48 resize: 256x48
type: BINARY type: BINARY
use_transparency: chroma_key transparency: chroma_key
color: color:
- id: light_blue - id: light_blue

View File

@@ -26,6 +26,18 @@ online_image:
format: PNG format: PNG
type: RGB type: RGB
transparency: chroma_key transparency: chroma_key
- id: online_binary_bmp
url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp
format: BMP
type: BINARY
- id: online_jpeg_image
url: http://www.faqs.org/images/library.jpg
format: JPEG
type: RGB
- id: online_jpg_image
url: http://www.faqs.org/images/library.jpg
format: JPG
type: RGB565
# Check the set_url action # Check the set_url action
esphome: esphome:

View File

@@ -7,6 +7,7 @@ udp:
encryption: "our key goes here" encryption: "our key goes here"
rolling_code_enable: true rolling_code_enable: true
ping_pong_enable: true ping_pong_enable: true
listen_address: 239.0.60.53
binary_sensors: binary_sensors:
- binary_sensor_id1 - binary_sensor_id1
- id: binary_sensor_id1 - id: binary_sensor_id1