Merge remote-tracking branch 'upstream/dev' into integration

# Conflicts:
#	esphome/components/api/api_connection.cpp
#	esphome/components/api/api_server.cpp
#	esphome/components/api/api_server.h
#	esphome/components/wifi_signal/wifi_signal_sensor.h
#	esphome/core/string_ref.h
This commit is contained in:
J. Nick Koston
2025-12-09 23:51:44 +01:00
89 changed files with 1634 additions and 384 deletions
+2 -2
View File
@@ -276,12 +276,12 @@ This document provides essential context for AI models interacting with this pro
## 7. Specific Instructions for AI Collaboration
* **Contribution Workflow (Pull Request Process):**
1. **Fork & Branch:** Create a new branch in your fork.
1. **Fork & Branch:** Create a new branch based on the `dev` branch (always use `git checkout -b <branch-name> dev` to ensure you're branching from `dev`, not the currently checked out branch).
2. **Make Changes:** Adhere to all coding conventions and patterns.
3. **Test:** Create component tests for all supported platforms and run the full test suite locally.
4. **Lint:** Run `pre-commit` to ensure code is compliant.
5. **Commit:** Commit your changes. There is no strict format for commit messages.
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made with the PULL_REQUEST_TEMPLATE.md template filled out correctly.
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
* **Documentation Contributions:**
* Documentation is hosted in the separate `esphome/esphome-docs` repository.
+1 -1
View File
@@ -1 +1 @@
c01eec15857a784dd603c0afd194ab3b29a632422fe6f6b0a806ad4d81b5efc0
766420905c06eeb6c5f360f68fd965e5ddd9c4a5db6b823263d3ad3accb64a07
+2
View File
@@ -212,6 +212,7 @@ esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hlk_fm22x/* @OnFreund
esphome/components/hlw8032/* @rici4kubicek
esphome/components/hm3301/* @freekode
esphome/components/hmac_md5/* @dwmw2
esphome/components/homeassistant/* @esphome/core @OttoWinter
@@ -523,6 +524,7 @@ esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter
esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon
esphome/components/usb_cdc_acm/* @kbx81
esphome/components/usb_host/* @clydebarrow
esphome/components/usb_uart/* @clydebarrow
esphome/components/valve/* @esphome/core
+1 -1
View File
@@ -2,7 +2,7 @@
We welcome contributions to the ESPHome suite of code and documentation!
Please read our [contributing guide](https://esphome.io/guides/contributing.html) if you wish to contribute to the
Please read our [contributing guide](https://developers.esphome.io/contributing/code/) if you wish to contribute to the
project and be sure to join us on [Discord](https://discord.gg/KhAMKrd).
**See also:**
@@ -163,7 +163,7 @@ float AbsoluteHumidityComponent::es_wobus(float t) {
}
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
// H/T to https://esphome.io/cookbook/bme280_environment.html
// H/T to https://esphome.io/cookbook/bme280_environment/
// H/T to https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
// es = saturated vapor pressure (kPa)
+1 -1
View File
@@ -246,7 +246,7 @@ def _validate_api_config(config: ConfigType) -> ConfigType:
_LOGGER.warning(
"API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. "
"Please migrate to the 'encryption' configuration. "
"See https://esphome.io/components/api.html#configuration-variables"
"See https://esphome.io/components/api/#configuration-variables"
)
return config
+7 -7
View File
@@ -1581,12 +1581,12 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
for (auto &it : this->parent_->get_state_subs()) {
// Compare entity_id and attribute with message fields
bool entity_match = (strcmp(it.entity_id_, msg.entity_id.c_str()) == 0);
bool attribute_match = (it.attribute_ != nullptr && strcmp(it.attribute_, msg.attribute.c_str()) == 0) ||
(it.attribute_ == nullptr && msg.attribute.empty());
bool entity_match = (strcmp(it.entity_id, msg.entity_id.c_str()) == 0);
bool attribute_match = (it.attribute != nullptr && strcmp(it.attribute, msg.attribute.c_str()) == 0) ||
(it.attribute == nullptr && msg.attribute.empty());
if (entity_match && attribute_match) {
it.callback_(msg.state);
it.callback(msg.state);
}
}
}
@@ -1962,12 +1962,12 @@ void APIConnection::process_state_subscriptions_() {
const auto &it = subs[this->state_subs_at_];
SubscribeHomeAssistantStateResponse resp;
resp.set_entity_id(StringRef(it.entity_id_));
resp.set_entity_id(StringRef(it.entity_id));
// Avoid string copy by using the const char* pointer if it exists
resp.set_attribute(it.attribute_ != nullptr ? StringRef(it.attribute_) : StringRef(""));
resp.set_attribute(it.attribute != nullptr ? StringRef(it.attribute) : StringRef(""));
resp.once = it.once_;
resp.once = it.once;
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
this->state_subs_at_++;
}
+9 -9
View File
@@ -423,8 +423,8 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
std::function<void(std::string)> f, bool once) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id_ = entity_id, .attribute_ = attribute, .callback_ = std::move(f), .once_ = once,
// entity_id_dynamic_storage_ and attribute_dynamic_storage_ remain nullptr (no heap allocation)
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
});
}
@@ -433,18 +433,18 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
std::function<void(std::string)> f, bool once) {
HomeAssistantStateSubscription sub;
// Allocate heap storage for the strings
sub.entity_id_dynamic_storage_ = std::make_unique<std::string>(std::move(entity_id));
sub.entity_id_ = sub.entity_id_dynamic_storage_->c_str();
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
sub.entity_id = sub.entity_id_dynamic_storage->c_str();
if (attribute.has_value()) {
sub.attribute_dynamic_storage_ = std::make_unique<std::string>(std::move(attribute.value()));
sub.attribute_ = sub.attribute_dynamic_storage_->c_str();
sub.attribute_dynamic_storage = std::make_unique<std::string>(std::move(attribute.value()));
sub.attribute = sub.attribute_dynamic_storage->c_str();
} else {
sub.attribute_ = nullptr;
sub.attribute = nullptr;
}
sub.callback_ = std::move(f);
sub.once_ = once;
sub.callback = std::move(f);
sub.once = once;
this->state_subs_.push_back(std::move(sub));
}
+6 -6
View File
@@ -190,15 +190,15 @@ class APIServer : public Component,
#ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription {
const char *entity_id_; // Pointer to flash (internal) or heap (external)
const char *attribute_; // Pointer to flash or nullptr (nullptr means no attribute)
std::function<void(std::string)> callback_;
bool once_;
const char *entity_id; // Pointer to flash (internal) or heap (external)
const char *attribute; // Pointer to flash or nullptr (nullptr means no attribute)
std::function<void(std::string)> callback;
bool once;
// Dynamic storage for external components using std::string API (custom_api_device.h)
// These are only allocated when using the std::string overload (nullptr for const char* overload)
std::unique_ptr<std::string> entity_id_dynamic_storage_;
std::unique_ptr<std::string> attribute_dynamic_storage_;
std::unique_ptr<std::string> entity_id_dynamic_storage;
std::unique_ptr<std::string> attribute_dynamic_storage;
};
// New const char* overload (for internal components - zero allocation)
@@ -44,7 +44,7 @@ CONFIG_SCHEMA = (
cv.Optional(ble_client.CONF_BLE_CLIENT_ID): cv.invalid(
"The 'ble_client_id' option has been removed. Please migrate "
"to the new `bedjet_id` option in the `bedjet` component.\n"
"See https://esphome.io/components/climate/bedjet.html"
"See https://esphome.io/components/climate/bedjet/"
),
cv.Optional(CONF_TIME_ID): cv.invalid(
"The 'time_id' option has been moved to the `bedjet` component."
+9 -6
View File
@@ -41,6 +41,7 @@ AUTO_LOAD = ["split_buffer"]
DEPENDENCIES = ["spi"]
CONF_INIT_SEQUENCE_ID = "init_sequence_id"
CONF_MINIMUM_UPDATE_INTERVAL = "minimum_update_interval"
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
EPaperBase = epaper_spi_ns.class_(
@@ -71,6 +72,9 @@ TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
minimum_update_interval = update_interval(
model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s")
)
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
return (
display.FULL_DISPLAY_SCHEMA.extend(
@@ -90,9 +94,9 @@ def model_schema(config):
{
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
cv.Optional(
CONF_UPDATE_INTERVAL, default=cv.UNDEFINED
): update_interval,
cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All(
update_interval, cv.Range(min=minimum_update_interval)
),
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
@@ -153,9 +157,8 @@ def _final_validate(config):
else:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
config[CONF_UPDATE_INTERVAL] = core.TimePeriod(
seconds=60
).total_milliseconds
elif CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
return config
+5 -5
View File
@@ -286,7 +286,7 @@ void EPaperBase::initialise_() {
* @param y
* @return false if the coordinates are out of bounds
*/
bool EPaperBase::rotate_coordinates_(int &x, int &y) const {
bool EPaperBase::rotate_coordinates_(int &x, int &y) {
if (!this->get_clipping().inside(x, y))
return false;
if (this->transform_ & SWAP_XY)
@@ -297,6 +297,10 @@ bool EPaperBase::rotate_coordinates_(int &x, int &y) const {
y = this->height_ - y - 1;
if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
return false;
this->x_low_ = clamp_at_most(this->x_low_, x);
this->x_high_ = clamp_at_least(this->x_high_, x + 1);
this->y_low_ = clamp_at_most(this->y_low_, y);
this->y_high_ = clamp_at_least(this->y_high_, y + 1);
return true;
}
@@ -319,10 +323,6 @@ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
} else {
this->buffer_[byte_position] = original | pixel_bit;
}
this->x_low_ = clamp_at_most(this->x_low_, x);
this->x_high_ = clamp_at_least(this->x_high_, x + 1);
this->y_low_ = clamp_at_most(this->y_low_, y);
this->y_high_ = clamp_at_least(this->y_high_, y + 1);
}
void EPaperBase::dump_config() {
+1 -1
View File
@@ -106,7 +106,7 @@ class EPaperBase : public Display,
void initialise_();
void wait_for_idle_(bool should_wait);
bool init_buffer_(size_t buffer_length);
bool rotate_coordinates_(int &x, int &y) const;
bool rotate_coordinates_(int &x, int &y);
/**
* Methods that must be implemented by concrete classes to control the display
@@ -4,8 +4,8 @@ from . import EpaperModel
class SpectraE6(EpaperModel):
def __init__(self, name, class_name="EPaperSpectraE6", **kwargs):
super().__init__(name, class_name, **kwargs)
def __init__(self, name, class_name="EPaperSpectraE6", **defaults):
super().__init__(name, class_name, **defaults)
# fmt: off
def get_init_sequence(self, config: dict):
@@ -30,7 +30,7 @@ class SpectraE6(EpaperModel):
return self.defaults.get(key, fallback)
spectra_e6 = SpectraE6("spectra-e6")
spectra_e6 = SpectraE6("spectra-e6", minimum_update_interval="30s")
spectra_e6_7p3 = spectra_e6.extend(
"7.3in-Spectra-E6",
+1 -1
View File
@@ -778,7 +778,7 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
+ "Need help? Check out the migration guide:\n"
+ color(
AnsiFore.BLUE,
"https://esphome.io/guides/esp32_arduino_to_idf.html",
"https://esphome.io/guides/esp32_arduino_to_idf/",
)
)
_LOGGER.warning(message)
+22
View File
@@ -1218,6 +1218,28 @@ ESP32_BOARD_PINS = {
"LED_BUILTINB": 4,
},
"sensesiot_weizen": {},
"seeed_xiao_esp32c6": {
"D0": 0,
"D1": 1,
"D2": 2,
"D3": 21,
"D4": 22,
"D5": 23,
"D6": 16,
"D7": 17,
"D8": 19,
"D9": 20,
"D10": 18,
"MTDO": 7,
"MTCK": 6,
"MTDI": 5,
"MTMS": 4,
"BOOT": 9,
"LED": 8,
"LED_BUILTIN": 8,
"RF_SWITCH_EN": 3,
"RF_ANT_SELECT": 14,
},
"sg-o_airMon": {},
"sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16},
"tinypico": {},
+14 -1
View File
@@ -1,9 +1,12 @@
import logging
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
from esphome.pins import check_strapping_pin
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c5/include/hal/i2c_ll.h
_ESP32C5_I2C_LP_PINS = {"SDA": 2, "SCL": 3}
_ESP32C5_SPI_PSRAM_PINS = {
16: "SPICS0",
17: "SPIQ",
@@ -43,3 +46,13 @@ def esp32_c5_validate_supports(value):
check_strapping_pin(value, _ESP32C5_STRAPPING_PINS, _LOGGER)
return value
def esp32_c5_validate_lp_i2c(value):
lp_sda_pin = _ESP32C5_I2C_LP_PINS["SDA"]
lp_scl_pin = _ESP32C5_I2C_LP_PINS["SCL"]
if int(value[CONF_SDA]) != lp_sda_pin or int(value[CONF_SCL]) != lp_scl_pin:
raise cv.Invalid(
f"Low power i2c interface is only supported on GPIO{lp_sda_pin} SDA and GPIO{lp_scl_pin} SCL for ESP32-C5"
)
return value
+14 -1
View File
@@ -1,9 +1,12 @@
import logging
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
from esphome.pins import check_strapping_pin
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c6/include/hal/i2c_ll.h
_ESP32C6_I2C_LP_PINS = {"SDA": 6, "SCL": 7}
_ESP32C6_SPI_PSRAM_PINS = {
24: "SPICS0",
25: "SPIQ",
@@ -43,3 +46,13 @@ def esp32_c6_validate_supports(value):
check_strapping_pin(value, _ESP32C6_STRAPPING_PINS, _LOGGER)
return value
def esp32_c6_validate_lp_i2c(value):
lp_sda_pin = _ESP32C6_I2C_LP_PINS["SDA"]
lp_scl_pin = _ESP32C6_I2C_LP_PINS["SCL"]
if int(value[CONF_SDA]) != lp_sda_pin or int(value[CONF_SCL]) != lp_scl_pin:
raise cv.Invalid(
f"Low power i2c interface is only supported on GPIO{lp_sda_pin} SDA and GPIO{lp_scl_pin} SCL for ESP32-C6"
)
return value
+15 -1
View File
@@ -1,9 +1,12 @@
import logging
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
from esphome.pins import check_strapping_pin
# https://documentation.espressif.com/esp32-p4-chip-revision-v1.3_datasheet_en.pdf
_ESP32P4_LP_PINS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
_ESP32P4_USB_JTAG_PINS = {24, 25}
_ESP32P4_STRAPPING_PINS = {34, 35, 36, 37, 38}
@@ -36,3 +39,14 @@ def esp32_p4_validate_supports(value):
pass
check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER)
return value
def esp32_p4_validate_lp_i2c(value):
if (
int(value[CONF_SDA]) not in _ESP32P4_LP_PINS
or int(value[CONF_SCL]) not in _ESP32P4_LP_PINS
):
raise cv.Invalid(
f"Low power i2c interface for ESP32-P4 is only supported on low power interface GPIO{min(_ESP32P4_LP_PINS)} - GPIO{max(_ESP32P4_LP_PINS)}"
)
return value
+1
View File
@@ -0,0 +1 @@
CODEOWNERS = ["@rici4kubicek"]
+194
View File
@@ -0,0 +1,194 @@
#include "hlw8032.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome::hlw8032 {
static const char *const TAG = "hlw8032";
static constexpr uint8_t STATE_REG_OFFSET = 0;
static constexpr uint8_t VOLTAGE_PARAM_OFFSET = 2;
static constexpr uint8_t VOLTAGE_REG_OFFSET = 5;
static constexpr uint8_t CURRENT_PARAM_OFFSET = 8;
static constexpr uint8_t CURRENT_REG_OFFSET = 11;
static constexpr uint8_t POWER_PARAM_OFFSET = 14;
static constexpr uint8_t POWER_REG_OFFSET = 17;
static constexpr uint8_t DATA_UPDATE_REG_OFFSET = 20;
static constexpr uint8_t CHECKSUM_REG_OFFSET = 23;
static constexpr uint8_t PARAM_REG_USABLE_BIT = (1 << 0);
static constexpr uint8_t POWER_OVERFLOW_BIT = (1 << 1);
static constexpr uint8_t CURRENT_OVERFLOW_BIT = (1 << 2);
static constexpr uint8_t VOLTAGE_OVERFLOW_BIT = (1 << 3);
static constexpr uint8_t HAVE_POWER_BIT = (1 << 4);
static constexpr uint8_t HAVE_CURRENT_BIT = (1 << 5);
static constexpr uint8_t HAVE_VOLTAGE_BIT = (1 << 6);
static constexpr uint8_t CHECK_REG = 0x5A;
static constexpr uint8_t STATE_REG_CORRECTION_FUNC_NORMAL = 0x55;
static constexpr uint8_t STATE_REG_CORRECTION_FUNC_FAIL = 0xAA;
static constexpr uint8_t STATE_REG_CORRECTION_MASK = 0xF0;
static constexpr uint8_t STATE_REG_OVERFLOW_MASK = 0xF;
static constexpr uint8_t PACKET_LENGTH = 24;
void HLW8032Component::loop() {
while (this->available()) {
uint8_t data = this->read();
if (!this->header_found_) {
if ((data == STATE_REG_CORRECTION_FUNC_NORMAL) || (data == STATE_REG_CORRECTION_FUNC_FAIL) ||
(data & STATE_REG_CORRECTION_MASK) == STATE_REG_CORRECTION_MASK) {
this->header_found_ = true;
this->raw_data_[0] = data;
}
} else if (data == CHECK_REG) {
this->raw_data_[1] = data;
this->raw_data_index_ = 2;
this->check_ = 0;
} else if (this->raw_data_index_ >= 2 && this->raw_data_index_ < PACKET_LENGTH) {
this->raw_data_[this->raw_data_index_++] = data;
if (this->raw_data_index_ < PACKET_LENGTH) {
this->check_ += data;
} else if (this->raw_data_index_ == PACKET_LENGTH) {
if (this->check_ == this->raw_data_[CHECKSUM_REG_OFFSET]) {
this->parse_data_();
} else {
ESP_LOGW(TAG, "Invalid checksum: 0x%02X != 0x%02X", this->check_, this->raw_data_[CHECKSUM_REG_OFFSET]);
}
this->raw_data_index_ = 0;
this->header_found_ = false;
memset(this->raw_data_, 0, PACKET_LENGTH);
}
}
}
}
uint32_t HLW8032Component::read_uint24_(uint8_t offset) {
return encode_uint24(this->raw_data_[offset], this->raw_data_[offset + 1], this->raw_data_[offset + 2]);
}
void HLW8032Component::parse_data_() {
// Parse header
uint8_t state_reg = this->raw_data_[STATE_REG_OFFSET];
if (state_reg == STATE_REG_CORRECTION_FUNC_FAIL) {
ESP_LOGE(TAG, "The chip's function of error correction fails.");
return;
}
// Parse data frame
uint32_t voltage_parameter = this->read_uint24_(VOLTAGE_PARAM_OFFSET);
uint32_t voltage_reg = this->read_uint24_(VOLTAGE_REG_OFFSET);
uint32_t current_parameter = this->read_uint24_(CURRENT_PARAM_OFFSET);
uint32_t current_reg = this->read_uint24_(CURRENT_REG_OFFSET);
uint32_t power_parameter = this->read_uint24_(POWER_PARAM_OFFSET);
uint32_t power_reg = this->read_uint24_(POWER_REG_OFFSET);
uint8_t data_update_register = this->raw_data_[DATA_UPDATE_REG_OFFSET];
bool have_power = data_update_register & HAVE_POWER_BIT;
bool have_current = data_update_register & HAVE_CURRENT_BIT;
bool have_voltage = data_update_register & HAVE_VOLTAGE_BIT;
bool power_cycle_exceeds_range = false;
bool parameter_regs_usable = true;
if ((state_reg & STATE_REG_CORRECTION_MASK) == STATE_REG_CORRECTION_MASK) {
if (state_reg & STATE_REG_OVERFLOW_MASK) {
if (state_reg & VOLTAGE_OVERFLOW_BIT) {
have_voltage = false;
}
if (state_reg & CURRENT_OVERFLOW_BIT) {
have_current = false;
}
if (state_reg & POWER_OVERFLOW_BIT) {
have_power = false;
}
if (state_reg & PARAM_REG_USABLE_BIT) {
parameter_regs_usable = false;
}
ESP_LOGW(TAG,
"Reports: (0x%02X)\n"
" Voltage REG overflows: %s\n"
" Current REG overflows: %s\n"
" Power REG overflows: %s\n"
" Voltage/Current/Power Parameter REGs not usable: %s\n",
state_reg, YESNO(!have_voltage), YESNO(!have_current), YESNO(!have_power),
YESNO(!parameter_regs_usable));
if (!parameter_regs_usable) {
return;
}
}
power_cycle_exceeds_range = have_power;
}
ESP_LOGVV(TAG,
"Parsed data:\n"
" Voltage: Parameter REG 0x%06" PRIX32 ", REG 0x%06" PRIX32 "\n"
" Current: Parameter REG 0x%06" PRIX32 ", REG 0x%06" PRIX32 "\n"
" Power: Parameter REG 0x%06" PRIX32 ", REG 0x%06" PRIX32 "\n"
" Data Update: REG 0x%02" PRIX8 "\n",
voltage_parameter, voltage_reg, current_parameter, current_reg, power_parameter, power_reg,
data_update_register);
const float current_multiplier = 1 / (this->current_resistor_ * 1000);
float voltage = 0.0f;
if (have_voltage && voltage_reg) {
voltage = float(voltage_parameter) * this->voltage_divider_ / float(voltage_reg);
}
if (this->voltage_sensor_ != nullptr) {
this->voltage_sensor_->publish_state(voltage);
}
float power = 0.0f;
if (have_power && power_reg && !power_cycle_exceeds_range) {
power = (float(power_parameter) / float(power_reg)) * this->voltage_divider_ * current_multiplier;
}
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(power);
}
float current = 0.0f;
if (have_current && current_reg) {
current = float(current_parameter) * current_multiplier / float(current_reg);
}
if (this->current_sensor_ != nullptr) {
this->current_sensor_->publish_state(current);
}
float pf = NAN;
const float apparent_power = voltage * current;
if (have_voltage && have_current) {
if (have_power || power_cycle_exceeds_range) {
if (apparent_power > 0) {
pf = power / apparent_power;
if (pf < 0 || pf > 1) {
ESP_LOGD(TAG, "Impossible power factor: %.4f not in interval [0, 1]", pf);
pf = NAN;
}
} else if (apparent_power == 0 && power == 0) {
// No load, report ideal power factor
pf = 1.0f;
}
}
}
if (this->apparent_power_sensor_ != nullptr) {
this->apparent_power_sensor_->publish_state(apparent_power);
}
if (this->power_factor_sensor_ != nullptr) {
this->power_factor_sensor_->publish_state(pf);
}
}
void HLW8032Component::dump_config() {
ESP_LOGCONFIG(TAG,
"Configuration:\n"
" Current resistor: %.1f mΩ\n"
" Voltage Divider: %.3f",
this->current_resistor_ * 1000.0f, this->voltage_divider_);
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
LOG_SENSOR(" ", "Current", this->current_sensor_);
LOG_SENSOR(" ", "Power", this->power_sensor_);
LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
}
} // namespace esphome::hlw8032
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome::hlw8032 {
class HLW8032Component : public Component, public uart::UARTDevice {
public:
void loop() override;
void dump_config() override;
void set_current_resistor(float current_resistor) { this->current_resistor_ = current_resistor; }
void set_voltage_divider(float voltage_divider) { this->voltage_divider_ = voltage_divider; }
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; }
void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; }
void set_apparent_power_sensor(sensor::Sensor *apparent_power_sensor) {
this->apparent_power_sensor_ = apparent_power_sensor;
}
void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) {
this->power_factor_sensor_ = power_factor_sensor;
}
protected:
void parse_data_();
uint32_t read_uint24_(uint8_t offset);
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
sensor::Sensor *apparent_power_sensor_{nullptr};
sensor::Sensor *power_factor_sensor_{nullptr};
float current_resistor_{0.001f};
float voltage_divider_{1.720f};
uint8_t raw_data_[24]{};
uint8_t check_{0};
uint8_t raw_data_index_{0};
bool header_found_{false};
};
} // namespace esphome::hlw8032
+93
View File
@@ -0,0 +1,93 @@
import esphome.codegen as cg
from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import (
CONF_APPARENT_POWER,
CONF_CURRENT,
CONF_CURRENT_RESISTOR,
CONF_ID,
CONF_POWER,
CONF_POWER_FACTOR,
CONF_VOLTAGE,
CONF_VOLTAGE_DIVIDER,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
UNIT_AMPERE,
UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_WATT,
)
DEPENDENCIES = ["uart"]
hlw8032_ns = cg.esphome_ns.namespace("hlw8032")
HLW8032Component = hlw8032_ns.class_("HLW8032Component", cg.Component, uart.UARTDevice)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(HLW8032Component),
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_APPARENT_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance,
cv.Optional(CONF_VOLTAGE_DIVIDER, default=1.720): cv.positive_float,
}
).extend(uart.UART_DEVICE_SCHEMA)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"hlw8032", baud_rate=4800, require_rx=True, data_bits=8, parity="EVEN"
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if voltage_config := config.get(CONF_VOLTAGE):
sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(sens))
if current_config := config.get(CONF_CURRENT):
sens = await sensor.new_sensor(current_config)
cg.add(var.set_current_sensor(sens))
if power_config := config.get(CONF_POWER):
sens = await sensor.new_sensor(power_config)
cg.add(var.set_power_sensor(sens))
if apparent_power_config := config.get(CONF_APPARENT_POWER):
sens = await sensor.new_sensor(apparent_power_config)
cg.add(var.set_apparent_power_sensor(sens))
if power_factor_config := config.get(CONF_POWER_FACTOR):
sens = await sensor.new_sensor(power_factor_config)
cg.add(var.set_power_factor_sensor(sens))
cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR]))
cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER]))
+71
View File
@@ -2,6 +2,23 @@ import logging
from esphome import pins
import esphome.codegen as cg
from esphome.components import esp32
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
get_esp32_variant,
)
from esphome.components.esp32.gpio_esp32_c5 import esp32_c5_validate_lp_i2c
from esphome.components.esp32.gpio_esp32_c6 import esp32_c6_validate_lp_i2c
from esphome.components.esp32.gpio_esp32_p4 import esp32_p4_validate_lp_i2c
from esphome.components.zephyr import (
zephyr_add_overlay,
zephyr_add_prj_conf,
@@ -16,6 +33,7 @@ from esphome.const import (
CONF_I2C,
CONF_I2C_ID,
CONF_ID,
CONF_LOW_POWER_MODE,
CONF_SCAN,
CONF_SCL,
CONF_SDA,
@@ -40,6 +58,25 @@ IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component)
ZephyrI2CBus = i2c_ns.class_("ZephyrI2CBus", I2CBus, cg.Component)
I2CDevice = i2c_ns.class_("I2CDevice")
ESP32_I2C_CAPABILITIES = {
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/soc_caps.h
VARIANT_ESP32: {"NUM": 2, "HP": 2},
VARIANT_ESP32C2: {"NUM": 1, "HP": 1},
VARIANT_ESP32C3: {"NUM": 1, "HP": 1},
VARIANT_ESP32C5: {"NUM": 2, "HP": 1, "LP": 1},
VARIANT_ESP32C6: {"NUM": 2, "HP": 1, "LP": 1},
VARIANT_ESP32C61: {"NUM": 1, "HP": 1},
VARIANT_ESP32H2: {"NUM": 2, "HP": 2},
VARIANT_ESP32P4: {"NUM": 3, "HP": 2, "LP": 1},
VARIANT_ESP32S2: {"NUM": 2, "HP": 2},
VARIANT_ESP32S3: {"NUM": 2, "HP": 2},
}
VALIDATE_LP_I2C = {
VARIANT_ESP32C5: esp32_c5_validate_lp_i2c,
VARIANT_ESP32C6: esp32_c6_validate_lp_i2c,
VARIANT_ESP32P4: esp32_p4_validate_lp_i2c,
}
LP_I2C_VARIANT = list(VALIDATE_LP_I2C.keys())
CONF_SDA_PULLUP_ENABLED = "sda_pullup_enabled"
CONF_SCL_PULLUP_ENABLED = "scl_pullup_enabled"
@@ -91,6 +128,13 @@ CONFIG_SCHEMA = cv.All(
cv.positive_time_period,
),
cv.Optional(CONF_SCAN, default=True): cv.boolean,
cv.Optional(CONF_LOW_POWER_MODE): cv.All(
cv.only_on_esp32,
esp32.only_on_variant(
supported=LP_I2C_VARIANT, msg_prefix="Low power i2c"
),
cv.boolean,
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_NRF52]),
@@ -102,6 +146,31 @@ def _final_validate(config):
full_config = fv.full_config.get()[CONF_I2C]
if CORE.using_zephyr and len(full_config) > 1:
raise cv.Invalid("Second i2c is not implemented on Zephyr yet")
if CORE.using_esp_idf and get_esp32_variant() in ESP32_I2C_CAPABILITIES:
variant = get_esp32_variant()
max_num = ESP32_I2C_CAPABILITIES[variant]["NUM"]
if len(full_config) > max_num:
raise cv.Invalid(
f"The maximum number of i2c interfaces for {variant} is {max_num}"
)
if variant in LP_I2C_VARIANT:
max_lp_num = ESP32_I2C_CAPABILITIES[variant]["LP"]
max_hp_num = ESP32_I2C_CAPABILITIES[variant]["HP"]
lp_num = sum(
CONF_LOW_POWER_MODE in conf and conf[CONF_LOW_POWER_MODE]
for conf in full_config
)
hp_num = len(full_config) - lp_num
if CONF_LOW_POWER_MODE in config and config[CONF_LOW_POWER_MODE]:
VALIDATE_LP_I2C[variant](config)
if lp_num > max_lp_num:
raise cv.Invalid(
f"The maximum number of low power i2c interfaces for {variant} is {max_lp_num}"
)
if hp_num > max_hp_num:
raise cv.Invalid(
f"The maximum number of high power i2c interfaces for {variant} is {max_hp_num}"
)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -155,6 +224,8 @@ async def to_code(config):
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
if CORE.using_arduino and not CORE.is_esp32:
cg.add_library("Wire", None)
if CONF_LOW_POWER_MODE in config:
cg.add(var.set_lp_mode(bool(config[CONF_LOW_POWER_MODE])))
def i2c_device_schema(default_address):
+25 -16
View File
@@ -16,13 +16,10 @@ namespace i2c {
static const char *const TAG = "i2c.idf";
void IDFI2CBus::setup() {
static i2c_port_t next_port = I2C_NUM_0;
this->port_ = next_port;
if (this->port_ == I2C_NUM_MAX) {
ESP_LOGE(TAG, "No more than %u buses supported", I2C_NUM_MAX);
this->mark_failed();
return;
}
static i2c_port_t next_hp_port = I2C_NUM_0;
#if SOC_LP_I2C_SUPPORTED
static i2c_port_t next_lp_port = LP_I2C_NUM_0;
#endif
if (this->timeout_ > 13000) {
ESP_LOGW(TAG, "Using max allowed timeout: 13 ms");
@@ -31,23 +28,35 @@ void IDFI2CBus::setup() {
this->recover_();
next_port = (i2c_port_t) (next_port + 1);
i2c_master_bus_config_t bus_conf{};
memset(&bus_conf, 0, sizeof(bus_conf));
bus_conf.sda_io_num = gpio_num_t(sda_pin_);
bus_conf.scl_io_num = gpio_num_t(scl_pin_);
bus_conf.i2c_port = this->port_;
bus_conf.glitch_ignore_cnt = 7;
#if SOC_LP_I2C_SUPPORTED
if (this->port_ < SOC_HP_I2C_NUM) {
bus_conf.clk_source = I2C_CLK_SRC_DEFAULT;
} else {
if (this->lp_mode_) {
if ((next_lp_port - LP_I2C_NUM_0) == SOC_LP_I2C_NUM) {
ESP_LOGE(TAG, "No more than %u LP buses supported", SOC_LP_I2C_NUM);
this->mark_failed();
return;
}
this->port_ = next_lp_port;
next_lp_port = (i2c_port_t) (next_lp_port + 1);
bus_conf.lp_source_clk = LP_I2C_SCLK_DEFAULT;
}
#else
bus_conf.clk_source = I2C_CLK_SRC_DEFAULT;
} else {
#endif
if (next_hp_port == SOC_HP_I2C_NUM) {
ESP_LOGE(TAG, "No more than %u HP buses supported", SOC_HP_I2C_NUM);
this->mark_failed();
return;
}
this->port_ = next_hp_port;
next_hp_port = (i2c_port_t) (next_hp_port + 1);
bus_conf.clk_source = I2C_CLK_SRC_DEFAULT;
#if SOC_LP_I2C_SUPPORTED
}
#endif
bus_conf.i2c_port = this->port_;
bus_conf.flags.enable_internal_pullup = sda_pullup_enabled_ || scl_pullup_enabled_;
esp_err_t err = i2c_new_master_bus(&bus_conf, &this->bus_);
if (err != ESP_OK) {
+6
View File
@@ -30,6 +30,9 @@ class IDFI2CBus : public InternalI2CBus, public Component {
void set_scl_pullup_enabled(bool scl_pullup_enabled) { this->scl_pullup_enabled_ = scl_pullup_enabled; }
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
#if SOC_LP_I2C_SUPPORTED
void set_lp_mode(bool lp_mode) { this->lp_mode_ = lp_mode; }
#endif
int get_port() const override { return this->port_; }
@@ -48,6 +51,9 @@ class IDFI2CBus : public InternalI2CBus, public Component {
uint32_t frequency_{};
uint32_t timeout_ = 0;
bool initialized_ = false;
#if SOC_LP_I2C_SUPPORTED
bool lp_mode_ = false;
#endif
};
} // namespace i2c
@@ -2,5 +2,5 @@ import esphome.config_validation as cv
CONFIG_SCHEMA = cv.invalid(
"The kalman_combinator sensor has moved.\nPlease use the combination platform instead with type: kalman.\n"
"See https://esphome.io/components/sensor/combination.html"
"See https://esphome.io/components/sensor/combination/"
)
+1 -1
View File
@@ -498,12 +498,12 @@ void LvglComponent::setup() {
buf_bytes /= MIN_BUFFER_FRAC;
buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT
}
this->buffer_frac_ = frac;
if (buffer == nullptr) {
this->status_set_error(LOG_STR("Memory allocation failure"));
this->mark_failed();
return;
}
this->buffer_frac_ = frac;
lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels);
this->disp_drv_.hor_res = display->get_width();
this->disp_drv_.ver_res = display->get_height();
+15 -26
View File
@@ -17,22 +17,6 @@ DEFAULT_POLLING_INTERVAL = "60s"
micronova_ns = cg.esphome_ns.namespace(DOMAIN)
MicroNovaFunctions = micronova_ns.enum("MicroNovaFunctions", is_class=True)
MICRONOVA_FUNCTIONS_ENUM = {
"STOVE_FUNCTION_SWITCH": MicroNovaFunctions.STOVE_FUNCTION_SWITCH,
"STOVE_FUNCTION_ROOM_TEMPERATURE": MicroNovaFunctions.STOVE_FUNCTION_ROOM_TEMPERATURE,
"STOVE_FUNCTION_THERMOSTAT_TEMPERATURE": MicroNovaFunctions.STOVE_FUNCTION_THERMOSTAT_TEMPERATURE,
"STOVE_FUNCTION_FUMES_TEMPERATURE": MicroNovaFunctions.STOVE_FUNCTION_FUMES_TEMPERATURE,
"STOVE_FUNCTION_STOVE_POWER": MicroNovaFunctions.STOVE_FUNCTION_STOVE_POWER,
"STOVE_FUNCTION_FAN_SPEED": MicroNovaFunctions.STOVE_FUNCTION_FAN_SPEED,
"STOVE_FUNCTION_STOVE_STATE": MicroNovaFunctions.STOVE_FUNCTION_STOVE_STATE,
"STOVE_FUNCTION_MEMORY_ADDRESS_SENSOR": MicroNovaFunctions.STOVE_FUNCTION_MEMORY_ADDRESS_SENSOR,
"STOVE_FUNCTION_WATER_TEMPERATURE": MicroNovaFunctions.STOVE_FUNCTION_WATER_TEMPERATURE,
"STOVE_FUNCTION_WATER_PRESSURE": MicroNovaFunctions.STOVE_FUNCTION_WATER_PRESSURE,
"STOVE_FUNCTION_POWER_LEVEL": MicroNovaFunctions.STOVE_FUNCTION_POWER_LEVEL,
"STOVE_FUNCTION_CUSTOM": MicroNovaFunctions.STOVE_FUNCTION_CUSTOM,
}
MicroNova = micronova_ns.class_("MicroNova", cg.Component, uart.UARTDevice)
MicroNovaListener = micronova_ns.class_("MicroNovaListener", cg.PollingComponent)
@@ -56,19 +40,25 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
def MICRONOVA_ADDRESS_SCHEMA(
*,
default_memory_location: int,
default_memory_address: int,
default_memory_location: int | None = None,
default_memory_address: int | None = None,
is_polling_component: bool,
):
location_key = (
cv.Optional(CONF_MEMORY_LOCATION, default=default_memory_location)
if default_memory_location is not None
else cv.Required(CONF_MEMORY_LOCATION)
)
address_key = (
cv.Optional(CONF_MEMORY_ADDRESS, default=default_memory_address)
if default_memory_address is not None
else cv.Required(CONF_MEMORY_ADDRESS)
)
schema = cv.Schema(
{
cv.GenerateID(CONF_MICRONOVA_ID): cv.use_id(MicroNova),
cv.Optional(
CONF_MEMORY_LOCATION, default=default_memory_location
): cv.hex_int_range(),
cv.Optional(
CONF_MEMORY_ADDRESS, default=default_memory_address
): cv.hex_int_range(),
location_key: cv.hex_int_range(min=0x00, max=0x79),
address_key: cv.hex_int_range(min=0x00, max=0xFF),
}
)
if is_polling_component:
@@ -76,12 +66,11 @@ def MICRONOVA_ADDRESS_SCHEMA(
return schema
async def to_code_micronova_listener(mv, var, config, micronova_function):
async def to_code_micronova_listener(mv, var, config):
await cg.register_component(var, config)
cg.add(mv.register_micronova_listener(var))
cg.add(var.set_memory_location(config[CONF_MEMORY_LOCATION]))
cg.add(var.set_memory_address(config[CONF_MEMORY_ADDRESS]))
cg.add(var.set_function(micronova_function))
async def to_code(config):
@@ -8,7 +8,6 @@ from .. import (
CONF_MICRONOVA_ID,
MICRONOVA_ADDRESS_SCHEMA,
MicroNova,
MicroNovaFunctions,
micronova_ns,
)
@@ -25,8 +24,6 @@ CONFIG_SCHEMA = cv.Schema(
)
.extend(
MICRONOVA_ADDRESS_SCHEMA(
default_memory_location=0xA0,
default_memory_address=0x7D,
is_polling_component=False,
)
)
@@ -40,7 +37,6 @@ async def to_code(config):
if custom_button_config := config.get(CONF_CUSTOM_BUTTON):
bt = await button.new_button(custom_button_config, mv)
cg.add(bt.set_memory_location(custom_button_config.get(CONF_MEMORY_LOCATION)))
cg.add(bt.set_memory_address(custom_button_config.get(CONF_MEMORY_ADDRESS)))
cg.add(bt.set_memory_location(custom_button_config[CONF_MEMORY_LOCATION]))
cg.add(bt.set_memory_address(custom_button_config[CONF_MEMORY_ADDRESS]))
cg.add(bt.set_memory_data(custom_button_config[CONF_MEMORY_DATA]))
cg.add(bt.set_function(MicroNovaFunctions.STOVE_FUNCTION_CUSTOM))
@@ -3,13 +3,7 @@
namespace esphome::micronova {
void MicroNovaButton::press_action() {
switch (this->get_function()) {
case MicroNovaFunctions::STOVE_FUNCTION_CUSTOM:
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_);
break;
default:
break;
}
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_);
this->micronova_->request_update_listeners();
}
+6 -2
View File
@@ -3,6 +3,9 @@
namespace esphome::micronova {
static const int STOVE_REPLY_DELAY = 60;
static const uint8_t WRITE_BIT = 1 << 7; // 0x80
void MicroNovaBaseListener::dump_base_config() {
ESP_LOGCONFIG(TAG,
" Memory Location: %02X\n"
@@ -125,7 +128,8 @@ void MicroNova::write_address(uint8_t location, uint8_t address, uint8_t data) {
uint16_t checksum = 0;
if (this->reply_pending_mutex_.try_lock()) {
write_data[0] = location;
uint8_t write_location = location | WRITE_BIT;
write_data[0] = write_location;
write_data[1] = address;
write_data[2] = data;
@@ -140,7 +144,7 @@ void MicroNova::write_address(uint8_t location, uint8_t address, uint8_t data) {
this->enable_rx_pin_->digital_write(false);
this->current_transmission_.request_transmission_time = millis();
this->current_transmission_.memory_location = location;
this->current_transmission_.memory_location = write_location;
this->current_transmission_.memory_address = address;
this->current_transmission_.reply_pending = true;
this->current_transmission_.initiating_listener = nullptr;
-41
View File
@@ -11,23 +11,6 @@
namespace esphome::micronova {
static const char *const TAG = "micronova";
static const int STOVE_REPLY_DELAY = 60;
enum class MicroNovaFunctions {
STOVE_FUNCTION_VOID = 0,
STOVE_FUNCTION_SWITCH = 1,
STOVE_FUNCTION_ROOM_TEMPERATURE = 2,
STOVE_FUNCTION_THERMOSTAT_TEMPERATURE = 3,
STOVE_FUNCTION_FUMES_TEMPERATURE = 4,
STOVE_FUNCTION_STOVE_POWER = 5,
STOVE_FUNCTION_FAN_SPEED = 6,
STOVE_FUNCTION_STOVE_STATE = 7,
STOVE_FUNCTION_MEMORY_ADDRESS_SENSOR = 8,
STOVE_FUNCTION_WATER_TEMPERATURE = 9,
STOVE_FUNCTION_WATER_PRESSURE = 10,
STOVE_FUNCTION_POWER_LEVEL = 11,
STOVE_FUNCTION_CUSTOM = 12
};
class MicroNova;
@@ -40,9 +23,6 @@ class MicroNovaBaseListener {
void set_micronova_object(MicroNova *m) { this->micronova_ = m; }
void set_function(MicroNovaFunctions f) { this->function_ = f; }
MicroNovaFunctions get_function() { return this->function_; }
void set_memory_location(uint8_t l) { this->memory_location_ = l; }
uint8_t get_memory_location() { return this->memory_location_; }
@@ -53,7 +33,6 @@ class MicroNovaBaseListener {
protected:
MicroNova *micronova_{nullptr};
MicroNovaFunctions function_ = MicroNovaFunctions::STOVE_FUNCTION_VOID;
uint8_t memory_location_ = 0;
uint8_t memory_address_ = 0;
};
@@ -76,17 +55,6 @@ class MicroNovaListener : public MicroNovaBaseListener, public PollingComponent
bool needs_update_ = false;
};
class MicroNovaSwitchListener : public MicroNovaBaseListener {
public:
MicroNovaSwitchListener(MicroNova *m) : MicroNovaBaseListener(m) {}
virtual void set_stove_state(bool v) = 0;
virtual bool get_stove_state() = 0;
protected:
uint8_t memory_data_on_ = 0;
uint8_t memory_data_off_ = 0;
};
class MicroNovaButtonListener : public MicroNovaBaseListener {
public:
MicroNovaButtonListener(MicroNova *m) : MicroNovaBaseListener(m) {}
@@ -113,15 +81,7 @@ class MicroNova : public Component, public uart::UARTDevice {
void set_enable_rx_pin(GPIOPin *enable_rx_pin) { this->enable_rx_pin_ = enable_rx_pin; }
void set_current_stove_state(uint8_t s) { this->current_stove_state_ = s; }
uint8_t get_current_stove_state() { return this->current_stove_state_; }
void set_stove(MicroNovaSwitchListener *s) { this->stove_switch_ = s; }
MicroNovaSwitchListener *get_stove_switch() { return this->stove_switch_; }
protected:
uint8_t current_stove_state_ = 0;
GPIOPin *enable_rx_pin_{nullptr};
struct MicroNovaSerialTransmission {
@@ -136,7 +96,6 @@ class MicroNova : public Component, public uart::UARTDevice {
MicroNovaSerialTransmission current_transmission_;
std::vector<MicroNovaListener *> micronova_listeners_{};
MicroNovaSwitchListener *stove_switch_{nullptr};
};
} // namespace esphome::micronova
@@ -7,7 +7,6 @@ from .. import (
CONF_MICRONOVA_ID,
MICRONOVA_ADDRESS_SCHEMA,
MicroNova,
MicroNovaFunctions,
MicroNovaListener,
micronova_ns,
to_code_micronova_listener,
@@ -17,7 +16,6 @@ ICON_FLASH = "mdi:flash"
CONF_THERMOSTAT_TEMPERATURE = "thermostat_temperature"
CONF_POWER_LEVEL = "power_level"
CONF_MEMORY_WRITE_LOCATION = "memory_write_location"
MicroNovaNumber = micronova_ns.class_(
"MicroNovaNumber", number.Number, MicroNovaListener
@@ -40,25 +38,18 @@ CONFIG_SCHEMA = cv.Schema(
)
.extend(
{
cv.Optional(
CONF_MEMORY_WRITE_LOCATION, default=0xA0
): cv.hex_int_range(),
cv.Optional(CONF_STEP, default=1.0): cv.float_range(min=0.1, max=10.0),
}
),
cv.Optional(CONF_POWER_LEVEL): number.number_schema(
MicroNovaNumber,
icon=ICON_FLASH,
)
.extend(
).extend(
MICRONOVA_ADDRESS_SCHEMA(
default_memory_location=0x20,
default_memory_address=0x7F,
is_polling_component=True,
)
)
.extend(
{cv.Optional(CONF_MEMORY_WRITE_LOCATION, default=0xA0): cv.hex_int_range()}
),
}
)
@@ -74,18 +65,9 @@ async def to_code(config):
max_value=40,
step=thermostat_temperature_config.get(CONF_STEP),
)
await to_code_micronova_listener(
mv,
numb,
thermostat_temperature_config,
MicroNovaFunctions.STOVE_FUNCTION_THERMOSTAT_TEMPERATURE,
)
await to_code_micronova_listener(mv, numb, thermostat_temperature_config)
cg.add(numb.set_micronova_object(mv))
cg.add(
numb.set_memory_write_location(
thermostat_temperature_config.get(CONF_MEMORY_WRITE_LOCATION)
)
)
cg.add(numb.set_use_step_scaling(True))
if power_level_config := config.get(CONF_POWER_LEVEL):
numb = await number.new_number(
@@ -94,12 +76,5 @@ async def to_code(config):
max_value=5,
step=1,
)
await to_code_micronova_listener(
mv, numb, power_level_config, MicroNovaFunctions.STOVE_FUNCTION_POWER_LEVEL
)
await to_code_micronova_listener(mv, numb, power_level_config)
cg.add(numb.set_micronova_object(mv))
cg.add(
numb.set_memory_write_location(
power_level_config.get(CONF_MEMORY_WRITE_LOCATION)
)
)
@@ -3,40 +3,26 @@
namespace esphome::micronova {
void MicroNovaNumber::process_value_from_stove(int value_from_stove) {
float new_sensor_value = 0;
if (value_from_stove == -1) {
this->publish_state(NAN);
return;
}
switch (this->get_function()) {
case MicroNovaFunctions::STOVE_FUNCTION_THERMOSTAT_TEMPERATURE:
new_sensor_value = ((float) value_from_stove) * this->traits.get_step();
break;
case MicroNovaFunctions::STOVE_FUNCTION_POWER_LEVEL:
new_sensor_value = (float) value_from_stove;
break;
default:
break;
float new_value = static_cast<float>(value_from_stove);
if (this->use_step_scaling_) {
new_value *= this->traits.get_step();
}
this->publish_state(new_sensor_value);
this->publish_state(new_value);
}
void MicroNovaNumber::control(float value) {
uint8_t new_number = 0;
switch (this->get_function()) {
case MicroNovaFunctions::STOVE_FUNCTION_THERMOSTAT_TEMPERATURE:
new_number = (uint8_t) (value / this->traits.get_step());
break;
case MicroNovaFunctions::STOVE_FUNCTION_POWER_LEVEL:
new_number = (uint8_t) value;
break;
default:
break;
uint8_t new_number;
if (this->use_step_scaling_) {
new_number = static_cast<uint8_t>(value / this->traits.get_step());
} else {
new_number = static_cast<uint8_t>(value);
}
this->micronova_->write_address(this->memory_write_location_, this->memory_address_, new_number);
this->micronova_->write_address(this->memory_location_, this->memory_address_, new_number);
this->micronova_->request_update_listeners();
}
@@ -19,11 +19,10 @@ class MicroNovaNumber : public number::Number, public MicroNovaListener {
}
void process_value_from_stove(int value_from_stove) override;
void set_memory_write_location(uint8_t l) { this->memory_write_location_ = l; }
uint8_t get_memory_write_location() { return this->memory_write_location_; }
void set_use_step_scaling(bool v) { this->use_step_scaling_ = v; }
protected:
uint8_t memory_write_location_ = 0;
bool use_step_scaling_ = false;
};
} // namespace esphome::micronova
+11 -14
View File
@@ -13,7 +13,6 @@ from .. import (
CONF_MICRONOVA_ID,
MICRONOVA_ADDRESS_SCHEMA,
MicroNova,
MicroNovaFunctions,
MicroNovaListener,
micronova_ns,
to_code_micronova_listener,
@@ -119,8 +118,6 @@ CONFIG_SCHEMA = cv.Schema(
MicroNovaSensor,
).extend(
MICRONOVA_ADDRESS_SCHEMA(
default_memory_location=0x00,
default_memory_address=0x00,
is_polling_component=True,
)
),
@@ -131,21 +128,21 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
mv = await cg.get_variable(config[CONF_MICRONOVA_ID])
for key, fn in {
CONF_ROOM_TEMPERATURE: MicroNovaFunctions.STOVE_FUNCTION_ROOM_TEMPERATURE,
CONF_FUMES_TEMPERATURE: MicroNovaFunctions.STOVE_FUNCTION_FUMES_TEMPERATURE,
CONF_STOVE_POWER: MicroNovaFunctions.STOVE_FUNCTION_STOVE_POWER,
CONF_MEMORY_ADDRESS_SENSOR: MicroNovaFunctions.STOVE_FUNCTION_MEMORY_ADDRESS_SENSOR,
CONF_WATER_TEMPERATURE: MicroNovaFunctions.STOVE_FUNCTION_WATER_TEMPERATURE,
CONF_WATER_PRESSURE: MicroNovaFunctions.STOVE_FUNCTION_WATER_PRESSURE,
for key, divisor in {
CONF_ROOM_TEMPERATURE: 2,
CONF_FUMES_TEMPERATURE: None,
CONF_STOVE_POWER: None,
CONF_MEMORY_ADDRESS_SENSOR: None,
CONF_WATER_TEMPERATURE: 2,
CONF_WATER_PRESSURE: 10,
}.items():
if sensor_config := config.get(key):
sens = await sensor.new_sensor(sensor_config, mv)
await to_code_micronova_listener(mv, sens, sensor_config, fn)
await to_code_micronova_listener(mv, sens, sensor_config)
if divisor:
cg.add(sens.set_divisor(divisor))
if fan_speed_config := config.get(CONF_FAN_SPEED):
sens = await sensor.new_sensor(fan_speed_config, mv)
await to_code_micronova_listener(
mv, sens, fan_speed_config, MicroNovaFunctions.STOVE_FUNCTION_FAN_SPEED
)
await to_code_micronova_listener(mv, sens, fan_speed_config)
cg.add(sens.set_fan_speed_offset(fan_speed_config[CONF_FAN_RPM_OFFSET]))
@@ -8,25 +8,15 @@ void MicroNovaSensor::process_value_from_stove(int value_from_stove) {
return;
}
float new_sensor_value = (float) value_from_stove;
switch (this->get_function()) {
case MicroNovaFunctions::STOVE_FUNCTION_ROOM_TEMPERATURE:
new_sensor_value = new_sensor_value / 2;
break;
case MicroNovaFunctions::STOVE_FUNCTION_THERMOSTAT_TEMPERATURE:
break;
case MicroNovaFunctions::STOVE_FUNCTION_FAN_SPEED:
new_sensor_value = new_sensor_value == 0 ? 0 : (new_sensor_value * 10) + this->fan_speed_offset_;
break;
case MicroNovaFunctions::STOVE_FUNCTION_WATER_TEMPERATURE:
new_sensor_value = new_sensor_value / 2;
break;
case MicroNovaFunctions::STOVE_FUNCTION_WATER_PRESSURE:
new_sensor_value = new_sensor_value / 10;
break;
default:
break;
float new_sensor_value = static_cast<float>(value_from_stove);
// Fan speed has special calculation: value * 10 + offset (when non-zero)
if (this->is_fan_speed_) {
new_sensor_value = value_from_stove == 0 ? 0.0f : (new_sensor_value * 10) + this->fan_speed_offset_;
} else if (this->divisor_ > 1) {
new_sensor_value = new_sensor_value / this->divisor_;
}
this->publish_state(new_sensor_value);
}
@@ -18,11 +18,16 @@ class MicroNovaSensor : public sensor::Sensor, public MicroNovaListener {
}
void process_value_from_stove(int value_from_stove) override;
void set_fan_speed_offset(uint8_t f) { this->fan_speed_offset_ = f; }
uint8_t get_set_fan_speed_offset() { return this->fan_speed_offset_; }
void set_divisor(uint8_t d) { this->divisor_ = d; }
void set_fan_speed_offset(uint8_t offset) {
this->is_fan_speed_ = true;
this->fan_speed_offset_ = offset;
}
protected:
int fan_speed_offset_ = 0;
uint8_t divisor_ = 1;
uint8_t fan_speed_offset_ = 0;
bool is_fan_speed_ = false;
};
} // namespace esphome::micronova
@@ -4,20 +4,21 @@ import esphome.config_validation as cv
from esphome.const import ICON_POWER
from .. import (
CONF_MEMORY_ADDRESS,
CONF_MEMORY_LOCATION,
CONF_MICRONOVA_ID,
MICRONOVA_ADDRESS_SCHEMA,
MicroNova,
MicroNovaFunctions,
MicroNovaListener,
micronova_ns,
to_code_micronova_listener,
)
CONF_STOVE = "stove"
CONF_MEMORY_DATA_ON = "memory_data_on"
CONF_MEMORY_DATA_OFF = "memory_data_off"
MicroNovaSwitch = micronova_ns.class_("MicroNovaSwitch", switch.Switch, cg.Component)
MicroNovaSwitch = micronova_ns.class_(
"MicroNovaSwitch", switch.Switch, MicroNovaListener
)
CONFIG_SCHEMA = cv.Schema(
{
@@ -28,9 +29,9 @@ CONFIG_SCHEMA = cv.Schema(
)
.extend(
MICRONOVA_ADDRESS_SCHEMA(
default_memory_location=0x80,
default_memory_location=0x00,
default_memory_address=0x21,
is_polling_component=False,
is_polling_component=True,
)
)
.extend(
@@ -48,9 +49,6 @@ async def to_code(config):
if stove_config := config.get(CONF_STOVE):
sw = await switch.new_switch(stove_config, mv)
cg.add(mv.set_stove(sw))
cg.add(sw.set_memory_location(stove_config[CONF_MEMORY_LOCATION]))
cg.add(sw.set_memory_address(stove_config[CONF_MEMORY_ADDRESS]))
await to_code_micronova_listener(mv, sw, stove_config)
cg.add(sw.set_memory_data_on(stove_config[CONF_MEMORY_DATA_ON]))
cg.add(sw.set_memory_data_off(stove_config[CONF_MEMORY_DATA_OFF]))
cg.add(sw.set_function(MicroNovaFunctions.STOVE_FUNCTION_SWITCH))
@@ -3,31 +3,36 @@
namespace esphome::micronova {
void MicroNovaSwitch::write_state(bool state) {
switch (this->get_function()) {
case MicroNovaFunctions::STOVE_FUNCTION_SWITCH:
if (state) {
// Only send power-on when current state is Off
if (this->micronova_->get_current_stove_state() == 0) {
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_);
this->publish_state(true);
} else {
ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", micronova_->get_current_stove_state());
}
} else {
// don't send power-off when status is Off or Final cleaning
if (this->micronova_->get_current_stove_state() != 0 && micronova_->get_current_stove_state() != 6) {
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_);
this->publish_state(false);
} else {
ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", micronova_->get_current_stove_state());
}
}
this->micronova_->request_update_listeners();
break;
default:
break;
if (state) {
// Only send power-on when current state is Off
if (this->raw_state_ == 0) {
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_);
this->publish_state(true);
} else {
ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", this->raw_state_);
}
} else {
// don't send power-off when status is Off or Final cleaning
if (this->raw_state_ != 0 && this->raw_state_ != 6) {
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_);
this->publish_state(false);
} else {
ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", this->raw_state_);
}
}
this->set_needs_update(true);
}
void MicroNovaSwitch::process_value_from_stove(int value_from_stove) {
this->raw_state_ = value_from_stove;
if (value_from_stove == -1) {
ESP_LOGE(TAG, "Error reading stove state");
return;
}
// set the stove switch to on for any value but 0
bool state = value_from_stove != 0;
this->publish_state(state);
}
} // namespace esphome::micronova
@@ -6,25 +6,28 @@
namespace esphome::micronova {
class MicroNovaSwitch : public Component, public switch_::Switch, public MicroNovaSwitchListener {
class MicroNovaSwitch : public switch_::Switch, public MicroNovaListener {
public:
MicroNovaSwitch(MicroNova *m) : MicroNovaSwitchListener(m) {}
MicroNovaSwitch(MicroNova *m) : MicroNovaListener(m) {}
void dump_config() override {
LOG_SWITCH("", "Micronova switch", this);
this->dump_base_config();
}
void set_stove_state(bool v) override { this->publish_state(v); }
bool get_stove_state() override { return this->state; }
void request_value_from_stove() override {
this->micronova_->request_address(this->memory_location_, this->memory_address_, this);
}
void process_value_from_stove(int value_from_stove) override;
void set_memory_data_on(uint8_t f) { this->memory_data_on_ = f; }
uint8_t get_memory_data_on() { return this->memory_data_on_; }
void set_memory_data_off(uint8_t f) { this->memory_data_off_ = f; }
uint8_t get_memory_data_off() { return this->memory_data_off_; }
protected:
void write_state(bool state) override;
uint8_t memory_data_on_ = 0;
uint8_t memory_data_off_ = 0;
uint8_t raw_state_ = 0;
};
} // namespace esphome::micronova
@@ -6,7 +6,6 @@ from .. import (
CONF_MICRONOVA_ID,
MICRONOVA_ADDRESS_SCHEMA,
MicroNova,
MicroNovaFunctions,
MicroNovaListener,
micronova_ns,
to_code_micronova_listener,
@@ -39,6 +38,4 @@ async def to_code(config):
if stove_state_config := config.get(CONF_STOVE_STATE):
sens = await text_sensor.new_text_sensor(stove_state_config, mv)
await to_code_micronova_listener(
mv, sens, stove_state_config, MicroNovaFunctions.STOVE_FUNCTION_STOVE_STATE
)
await to_code_micronova_listener(mv, sens, stove_state_config)
@@ -8,22 +8,7 @@ void MicroNovaTextSensor::process_value_from_stove(int value_from_stove) {
return;
}
switch (this->get_function()) {
case MicroNovaFunctions::STOVE_FUNCTION_STOVE_STATE:
this->micronova_->set_current_stove_state(value_from_stove);
this->publish_state(STOVE_STATES[value_from_stove]);
// set the stove switch to on for any value but 0
if (value_from_stove != 0 && this->micronova_->get_stove_switch() != nullptr &&
!this->micronova_->get_stove_switch()->get_stove_state()) {
this->micronova_->get_stove_switch()->set_stove_state(true);
} else if (value_from_stove == 0 && this->micronova_->get_stove_switch() != nullptr &&
this->micronova_->get_stove_switch()->get_stove_state()) {
this->micronova_->get_stove_switch()->set_stove_state(false);
}
break;
default:
break;
}
this->publish_state(STOVE_STATES[value_from_stove]);
}
} // namespace esphome::micronova
+5 -10
View File
@@ -24,7 +24,7 @@ from esphome.components.mipi import (
CONF_VSYNC_BACK_PORCH,
CONF_VSYNC_FRONT_PORCH,
CONF_VSYNC_PULSE_WIDTH,
MODE_BGR,
MODE_RGB,
PIXEL_MODE_16BIT,
PIXEL_MODE_18BIT,
DriverChip,
@@ -157,7 +157,7 @@ def model_schema(config):
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
pins.gpio_output_pin_schema
),
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(COLOR_ORDERS, upper=True),
model.option(CONF_COLOR_ORDER, MODE_RGB): cv.enum(COLOR_ORDERS, upper=True),
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of(
*pixel_modes, lower=True
@@ -280,14 +280,9 @@ async def to_code(config):
red_pins = config[CONF_DATA_PINS][CONF_RED]
green_pins = config[CONF_DATA_PINS][CONF_GREEN]
blue_pins = config[CONF_DATA_PINS][CONF_BLUE]
if config[CONF_COLOR_ORDER] == "BGR":
dpins.extend(red_pins)
dpins.extend(green_pins)
dpins.extend(blue_pins)
else:
dpins.extend(blue_pins)
dpins.extend(green_pins)
dpins.extend(red_pins)
dpins.extend(blue_pins)
dpins.extend(green_pins)
dpins.extend(red_pins)
# swap bytes to match big-endian format
dpins = dpins[8:16] + dpins[0:8]
else:
+4 -11
View File
@@ -371,17 +371,10 @@ void MipiRgb::dump_config() {
get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(),
get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str());
if (this->madctl_ & MADCTL_BGR) {
this->dump_pins_(8, 13, "Blue", 0);
this->dump_pins_(13, 16, "Green", 0);
this->dump_pins_(0, 3, "Green", 3);
this->dump_pins_(3, 8, "Red", 0);
} else {
this->dump_pins_(8, 13, "Red", 0);
this->dump_pins_(13, 16, "Green", 0);
this->dump_pins_(0, 3, "Green", 3);
this->dump_pins_(3, 8, "Blue", 0);
}
this->dump_pins_(8, 13, "Blue", 0);
this->dump_pins_(13, 16, "Green", 0);
this->dump_pins_(0, 3, "Green", 3);
this->dump_pins_(3, 8, "Red", 0);
}
} // namespace mipi_rgb
@@ -7,7 +7,6 @@ ST7701S(
"T-PANEL-S3",
width=480,
height=480,
color_order="BGR",
invert_colors=False,
swap_xy=UNDEFINED,
spi_mode="MODE3",
@@ -56,7 +55,6 @@ t_rgb = ST7701S(
"T-RGB-2.1",
width=480,
height=480,
color_order="BGR",
pixel_mode="18bit",
invert_colors=False,
swap_xy=UNDEFINED,
@@ -82,7 +82,6 @@ st7701s.extend(
"MAKERFABS-4",
width=480,
height=480,
color_order="RGB",
invert_colors=True,
pixel_mode="18bit",
cs_pin=1,
@@ -1,13 +1,13 @@
from esphome.components.mipi import DriverChip
from esphome.components.mipi import DriverChip, delay
from esphome.config_validation import UNDEFINED
from .st7701s import st7701s
# fmt: off
wave_4_3 = DriverChip(
"ESP32-S3-TOUCH-LCD-4.3",
swap_xy=UNDEFINED,
initsequence=(),
color_order="RGB",
width=800,
height=480,
pclk_frequency="16MHz",
@@ -55,10 +55,9 @@ wave_4_3.extend(
)
st7701s.extend(
"WAVESHARE-4-480x480",
"WAVESHARE-4-480X480",
data_rate="2MHz",
spi_mode="MODE3",
color_order="BGR",
pixel_mode="18bit",
width=480,
height=480,
@@ -76,3 +75,72 @@ st7701s.extend(
"blue": [5, 45, 48, 47, 21],
},
)
st7701s.extend(
"WAVESHARE-3.16-320X820",
width=320,
height=820,
de_pin=40,
hsync_pin=38,
vsync_pin=39,
pclk_pin=41,
cs_pin={
"number": 0,
"ignore_strapping_warning": True,
},
pclk_frequency="18MHz",
reset_pin=16,
hsync_back_porch=30,
hsync_front_porch=30,
hsync_pulse_width=6,
vsync_back_porch=20,
vsync_front_porch=20,
vsync_pulse_width=40,
data_pins={
"red": [17, 46, 3, 8, 18],
"green": [14, 13, 12, 11, 10, 9],
"blue": [21, 5, 45, 48, 47],
},
initsequence=(
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x13),
(0xEF, 0x08),
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x10),
(0xC0, 0xE5, 0x02),
(0xC1, 0x15, 0x0A),
(0xC2, 0x07, 0x02),
(0xCC, 0x10),
(0xB0, 0x00, 0x08, 0x51, 0x0D, 0xCE, 0x06, 0x00, 0x08, 0x08, 0x24, 0x05, 0xD0, 0x0F, 0x6F, 0x36, 0x1F),
(0xB1, 0x00, 0x10, 0x4F, 0x0C, 0x11, 0x05, 0x00, 0x07, 0x07, 0x18, 0x02, 0xD3, 0x11, 0x6E, 0x34, 0x1F),
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x11),
(0xB0, 0x4D),
(0xB1, 0x37),
(0xB2, 0x87),
(0xB3, 0x80),
(0xB5, 0x4A),
(0xB7, 0x85),
(0xB8, 0x21),
(0xB9, 0x00, 0x13),
(0xC0, 0x09),
(0xC1, 0x78),
(0xC2, 0x78),
(0xD0, 0x88),
(0xE0, 0x80, 0x00, 0x02),
(0xE1, 0x0F, 0xA0, 0x00, 0x00, 0x10, 0xA0, 0x00, 0x00, 0x00, 0x60, 0x60),
(0xE2, 0x30, 0x30, 0x60, 0x60, 0x45, 0xA0, 0x00, 0x00, 0x46, 0xA0, 0x00, 0x00, 0x00),
(0xE3, 0x00, 0x00, 0x33, 0x33),
(0xE4, 0x44, 0x44),
(0xE5, 0x0F, 0x4A, 0xA0, 0xA0, 0x11, 0x4A, 0xA0, 0xA0, 0x13, 0x4A, 0xA0, 0xA0, 0x15, 0x4A, 0xA0, 0xA0),
(0xE6, 0x00, 0x00, 0x33, 0x33),
(0xE7, 0x44, 0x44),
(0xE8, 0x10, 0x4A, 0xA0, 0xA0, 0x12, 0x4A, 0xA0, 0xA0, 0x14, 0x4A, 0xA0, 0xA0, 0x16, 0x4A, 0xA0, 0xA0),
(0xEB, 0x02, 0x00, 0x4E, 0x4E, 0xEE, 0x44, 0x00),
(0xED, 0xFF, 0xFF, 0x04, 0x56, 0x72, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, 0x65, 0x40, 0xFF, 0xFF),
(0xEF, 0x08, 0x08, 0x08, 0x40, 0x3F, 0x64),
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x13),
(0xE8, 0x00, 0x0E),
(0xE8, 0x00, 0x0C),
delay(10),
(0xE8, 0x00, 0x00),
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x00),
)
)
+10 -1
View File
@@ -91,7 +91,16 @@ def validate_source_shorthand(value):
def deprecate_single_package(config):
_LOGGER.warning(
"Including a single package under `packages:` is deprecated. Use a list instead."
"""
Including a single package under `packages:`, i.e., `packages: !include mypackage.yaml` is deprecated.
This method for including packages will go away in 2026.7.0
Please use a list instead:
packages:
- !include mypackage.yaml
See https://github.com/esphome/esphome/pull/12116
"""
)
return config
+16 -1
View File
@@ -1,7 +1,12 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_EXTERNAL_CLOCK_INPUT, CONF_FREQUENCY, CONF_ID
from esphome.const import (
CONF_EXTERNAL_CLOCK_INPUT,
CONF_FREQUENCY,
CONF_ID,
CONF_PHASE_BALANCER,
)
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
@@ -9,6 +14,12 @@ MULTI_CONF = True
pca9685_ns = cg.esphome_ns.namespace("pca9685")
PCA9685Output = pca9685_ns.class_("PCA9685Output", cg.Component, i2c.I2CDevice)
phase_balancer = pca9685_ns.enum("PhaseBalancer", is_class=True)
PHASE_BALANCERS = {
"none": phase_balancer.NONE,
"linear": phase_balancer.LINEAR,
}
def validate_frequency(config):
if config[CONF_EXTERNAL_CLOCK_INPUT]:
@@ -30,6 +41,9 @@ CONFIG_SCHEMA = cv.All(
cv.frequency, cv.Range(min=23.84, max=1525.88)
),
cv.Optional(CONF_EXTERNAL_CLOCK_INPUT, default=False): cv.boolean,
cv.Optional(CONF_PHASE_BALANCER, default="linear"): cv.enum(
PHASE_BALANCERS
),
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -43,5 +57,6 @@ async def to_code(config):
if CONF_FREQUENCY in config:
cg.add(var.set_frequency(config[CONF_FREQUENCY]))
cg.add(var.set_extclk(config[CONF_EXTERNAL_CLOCK_INPUT]))
cg.add(var.set_phase_balancer(config[CONF_PHASE_BALANCER]))
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
+12 -1
View File
@@ -105,7 +105,18 @@ void PCA9685Output::loop() {
const uint16_t num_channels = this->max_channel_ - this->min_channel_ + 1;
const uint16_t phase_delta_begin = 4096 / num_channels;
for (uint8_t channel = this->min_channel_; channel <= this->max_channel_; channel++) {
uint16_t phase_begin = (channel - this->min_channel_) * phase_delta_begin;
uint16_t phase_begin;
switch (this->balancer_) {
case PhaseBalancer::NONE:
phase_begin = 0;
break;
case PhaseBalancer::LINEAR:
phase_begin = (channel - this->min_channel_) * phase_delta_begin;
break;
default:
ESP_LOGE(TAG, "Unknown phase balancer %d", static_cast<int>(this->balancer_));
return;
}
uint16_t phase_end;
uint16_t amount = this->pwm_amounts_[channel];
if (amount == 0) {
@@ -7,6 +7,11 @@
namespace esphome {
namespace pca9685 {
enum class PhaseBalancer {
NONE = 0x00,
LINEAR = 0x01,
};
/// Inverts polarity of channel output signal
extern const uint8_t PCA9685_MODE_INVERTED;
/// Channel update happens upon ACK (post-set) rather than on STOP (endTransmission)
@@ -47,6 +52,7 @@ class PCA9685Output : public Component, public i2c::I2CDevice {
void loop() override;
void set_extclk(bool extclk) { this->extclk_ = extclk; }
void set_frequency(float frequency) { this->frequency_ = frequency; }
void set_phase_balancer(PhaseBalancer balancer) { this->balancer_ = balancer; }
protected:
friend PCA9685Channel;
@@ -60,6 +66,7 @@ class PCA9685Output : public Component, public i2c::I2CDevice {
float frequency_;
uint8_t mode_;
bool extclk_ = false;
PhaseBalancer balancer_ = PhaseBalancer::LINEAR;
uint8_t min_channel_{0xFF};
uint8_t max_channel_{0x00};
+1 -1
View File
@@ -55,7 +55,7 @@ def CONFIG_SCHEMA(conf):
if conf:
raise cv.Invalid(
"This component has been moved in 1.16, please see the docs for updated "
"instructions. https://esphome.io/components/binary_sensor/pn532.html"
"instructions. https://esphome.io/components/binary_sensor/pn532/"
)
-1
View File
@@ -197,7 +197,6 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_SPIRAM_SPEED", speed)
if config[CONF_MODE] == TYPE_OCTAL and speed == 120:
add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_FLASHFREQ_120M", True)
add_idf_sdkconfig_option("CONFIG_BOOTLOADER_FLASH_DC_AWARE", True)
if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 4, 0):
add_idf_sdkconfig_option(
"CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True
+1 -1
View File
@@ -4,5 +4,5 @@ CODEOWNERS = ["@SenexCrenshaw"]
CONFIG_SCHEMA = cv.invalid(
"SGP40 is deprecated.\nPlease use the SGP4x platform instead.\nSGP4x supports both SPG40 and SGP41.\n"
" See https://esphome.io/components/sensor/sgp4x.html"
" See https://esphome.io/components/sensor/sgp4x/"
)
+1 -1
View File
@@ -40,7 +40,7 @@ class SX1509Component : public Component,
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
float get_setup_priority() const override { return setup_priority::IO; }
void loop() override;
uint16_t read_key_data();
@@ -0,0 +1,76 @@
import esphome.codegen as cg
from esphome.components import esp32, uart
from esphome.components.esp32 import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
add_idf_sdkconfig_option,
)
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_RX_BUFFER_SIZE, CONF_TX_BUFFER_SIZE
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"]
AUTO_LOAD = ["uart"]
DEPENDENCIES = ["tinyusb"]
CONF_INTERFACES = "interfaces"
usb_cdc_acm_ns = cg.esphome_ns.namespace("usb_cdc_acm")
USBCDCACMComponent = usb_cdc_acm_ns.class_("USBCDCACMComponent", cg.Component)
USBCDCACMInstance = usb_cdc_acm_ns.class_(
"USBCDCACMInstance", uart.UARTComponent, cg.Parented.template(USBCDCACMComponent)
)
# Schema for individual CDC ACM interface instances
INTERFACE_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(USBCDCACMInstance),
}
)
# Main component schema
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(USBCDCACMComponent),
cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.All(
cv.validate_bytes, cv.uint16_t
),
cv.Optional(CONF_TX_BUFFER_SIZE, default=256): cv.All(
cv.validate_bytes, cv.uint16_t
),
cv.Optional(CONF_INTERFACES, default=[{}]): cv.All(
cv.ensure_list(INTERFACE_SCHEMA),
cv.Length(min=1, max=2), # At least 1, at most 2 interfaces
),
}
).extend(cv.COMPONENT_SCHEMA),
esp32.only_on_variant(
supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3],
),
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# Create and register interface instances
for interface_index, interface_conf in enumerate(config[CONF_INTERFACES]):
interface = cg.new_Pvariable(interface_conf[CONF_ID])
await cg.register_parented(interface, var)
cg.add(interface.set_interface_number(interface_index))
cg.add(var.add_interface(interface))
# Configure TinyUSB with the correct number of CDC interfaces
num_interfaces = len(config[CONF_INTERFACES])
add_idf_sdkconfig_option("CONFIG_TINYUSB_CDC_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_TINYUSB_CDC_COUNT", num_interfaces)
add_idf_sdkconfig_option(
"CONFIG_TINYUSB_CDC_RX_BUFSIZE", config[CONF_RX_BUFFER_SIZE]
)
add_idf_sdkconfig_option(
"CONFIG_TINYUSB_CDC_TX_BUFSIZE", config[CONF_TX_BUFFER_SIZE]
)
@@ -0,0 +1,495 @@
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "usb_cdc_acm.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/ringbuf.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "tusb.h"
#include "tusb_cdc_acm.h"
namespace esphome::usb_cdc_acm {
static const char *TAG = "usb_cdc_acm";
static constexpr size_t USB_TX_TASK_STACK_SIZE = 4096;
static constexpr size_t USB_TX_TASK_STACK_SIZE_VV = 8192;
// Global component instance for managing USB device
USBCDCACMComponent *global_usb_cdc_component = nullptr;
static USBCDCACMInstance *get_instance_by_itf(int itf) {
if (global_usb_cdc_component == nullptr) {
return nullptr;
}
return global_usb_cdc_component->get_interface_by_number(itf);
}
static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) {
USBCDCACMInstance *instance = get_instance_by_itf(itf);
if (instance == nullptr) {
ESP_LOGE(TAG, "RX callback: invalid interface %d", itf);
return;
}
size_t rx_size = 0;
static uint8_t rx_buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE] = {0};
// read from USB
esp_err_t ret =
tinyusb_cdcacm_read(static_cast<tinyusb_cdcacm_itf_t>(itf), rx_buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size);
ESP_LOGV(TAG, "tinyusb_cdc_rx_callback itf=%d (size: %u)", itf, rx_size);
ESP_LOGVV(TAG, "rx_buf = %s", format_hex_pretty(rx_buf, rx_size).c_str());
if (ret == ESP_OK && rx_size > 0) {
RingbufHandle_t rx_ringbuf = instance->get_rx_ringbuf();
if (rx_ringbuf != nullptr) {
BaseType_t send_res = xRingbufferSend(rx_ringbuf, rx_buf, rx_size, 0);
if (send_res != pdTRUE) {
ESP_LOGE(TAG, "USB RX itf=%d: buffer full, %u bytes lost", itf, rx_size);
} else {
ESP_LOGV(TAG, "USB RX itf=%d: queued %u bytes", itf, rx_size);
}
}
}
}
static void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event) {
USBCDCACMInstance *instance = get_instance_by_itf(itf);
if (instance == nullptr) {
ESP_LOGE(TAG, "Line state callback: invalid interface %d", itf);
return;
}
int dtr = event->line_state_changed_data.dtr;
int rts = event->line_state_changed_data.rts;
ESP_LOGV(TAG, "Line state itf=%d: DTR=%d, RTS=%d", itf, dtr, rts);
// Queue event for processing in main loop
instance->queue_line_state_event(dtr != 0, rts != 0);
}
static void tinyusb_cdc_line_coding_changed_callback(int itf, cdcacm_event_t *event) {
USBCDCACMInstance *instance = get_instance_by_itf(itf);
if (instance == nullptr) {
ESP_LOGE(TAG, "Line coding callback: invalid interface %d", itf);
return;
}
uint32_t bit_rate = event->line_coding_changed_data.p_line_coding->bit_rate;
uint8_t stop_bits = event->line_coding_changed_data.p_line_coding->stop_bits;
uint8_t parity = event->line_coding_changed_data.p_line_coding->parity;
uint8_t data_bits = event->line_coding_changed_data.p_line_coding->data_bits;
ESP_LOGV(TAG, "Line coding itf=%d: bit_rate=%" PRIu32 " stop_bits=%u parity=%u data_bits=%u", itf, bit_rate,
stop_bits, parity, data_bits);
// Queue event for processing in main loop
instance->queue_line_coding_event(bit_rate, stop_bits, parity, data_bits);
}
static esp_err_t ringbuf_read_bytes(RingbufHandle_t ring_buf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size,
TickType_t xTicksToWait) {
size_t read_sz;
uint8_t *buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, xTicksToWait, out_buf_sz));
if (buf == nullptr) {
return ESP_FAIL;
}
memcpy(out_buf, buf, read_sz);
vRingbufferReturnItem(ring_buf, (void *) buf);
*rx_data_size = read_sz;
// Buffer's data can be wrapped, in which case we should perform another read
buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, 0, out_buf_sz - *rx_data_size));
if (buf != nullptr) {
memcpy(out_buf + *rx_data_size, buf, read_sz);
vRingbufferReturnItem(ring_buf, (void *) buf);
*rx_data_size += read_sz;
}
return ESP_OK;
}
//==============================================================================
// USBCDCACMInstance Implementation
//==============================================================================
void USBCDCACMInstance::setup() {
this->usb_tx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_TX_BUFSIZE, RINGBUF_TYPE_BYTEBUF);
if (this->usb_tx_ringbuf_ == nullptr) {
ESP_LOGE(TAG, "USB TX buffer creation error for itf %d", this->itf_);
this->parent_->mark_failed();
return;
}
this->usb_rx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_RX_BUFSIZE, RINGBUF_TYPE_BYTEBUF);
if (this->usb_rx_ringbuf_ == nullptr) {
ESP_LOGE(TAG, "USB RX buffer creation error for itf %d", this->itf_);
this->parent_->mark_failed();
return;
}
// Configure this CDC interface
const tinyusb_config_cdcacm_t acm_cfg = {
.usb_dev = TINYUSB_USBDEV_0,
.cdc_port = this->itf_,
.callback_rx = &tinyusb_cdc_rx_callback,
.callback_rx_wanted_char = NULL,
.callback_line_state_changed = &tinyusb_cdc_line_state_changed_callback,
.callback_line_coding_changed = &tinyusb_cdc_line_coding_changed_callback,
};
esp_err_t result = tusb_cdc_acm_init(&acm_cfg);
if (result != ESP_OK) {
ESP_LOGE(TAG, "tusb_cdc_acm_init failed: %d", result);
this->parent_->mark_failed();
return;
}
// Use a larger stack size for (very) verbose logging
const size_t stack_size = esp_log_level_get(TAG) > ESP_LOG_DEBUG ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE;
// Create a simple, unique task name per interface
char task_name[] = "usb_tx_0";
task_name[sizeof(task_name) - 1] = format_hex_char(static_cast<char>(this->itf_));
xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, 4, &this->usb_tx_task_handle_);
if (this->usb_tx_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Failed to create USB TX task for itf %d", this->itf_);
this->parent_->mark_failed();
return;
}
}
void USBCDCACMInstance::loop() {
// Process events from the lock-free queue
this->process_events_();
}
void USBCDCACMInstance::queue_line_state_event(bool dtr, bool rts) {
// Allocate event from pool
CDCEvent *event = this->event_pool_.allocate();
if (event == nullptr) {
ESP_LOGW(TAG, "Event pool exhausted, line state event dropped (itf=%d)", this->itf_);
return;
}
event->type = CDC_EVENT_LINE_STATE_CHANGED;
event->data.line_state.dtr = dtr;
event->data.line_state.rts = rts;
if (!this->event_queue_.push(event)) {
ESP_LOGW(TAG, "Event queue full, line state event dropped (itf=%d)", this->itf_);
// Return event to pool since we couldn't queue it
this->event_pool_.release(event);
} else {
// Wake main loop immediately to process event
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
}
}
void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity,
uint8_t data_bits) {
// Allocate event from pool
CDCEvent *event = this->event_pool_.allocate();
if (event == nullptr) {
ESP_LOGW(TAG, "Event pool exhausted, line coding event dropped (itf=%d)", this->itf_);
return;
}
event->type = CDC_EVENT_LINE_CODING_CHANGED;
event->data.line_coding.bit_rate = bit_rate;
event->data.line_coding.stop_bits = stop_bits;
event->data.line_coding.parity = parity;
event->data.line_coding.data_bits = data_bits;
if (!this->event_queue_.push(event)) {
ESP_LOGW(TAG, "Event queue full, line coding event dropped (itf=%d)", this->itf_);
// Return event to pool since we couldn't queue it
this->event_pool_.release(event);
} else {
// Wake main loop immediately to process event
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
}
}
void USBCDCACMInstance::process_events_() {
// Process all pending events from the queue
CDCEvent *event;
while ((event = this->event_queue_.pop()) != nullptr) {
switch (event->type) {
case CDC_EVENT_LINE_STATE_CHANGED: {
bool dtr = event->data.line_state.dtr;
bool rts = event->data.line_state.rts;
// Invoke user callback in main loop context
if (this->line_state_callback_ != nullptr) {
this->line_state_callback_(dtr, rts);
}
break;
}
case CDC_EVENT_LINE_CODING_CHANGED: {
uint32_t bit_rate = event->data.line_coding.bit_rate;
uint8_t stop_bits = event->data.line_coding.stop_bits;
uint8_t parity = event->data.line_coding.parity;
uint8_t data_bits = event->data.line_coding.data_bits;
// Update UART configuration based on CDC line coding
this->baud_rate_ = bit_rate;
this->data_bits_ = data_bits;
// Convert CDC stop bits to UART stop bits format
// CDC: 0=1 stop bit, 1=1.5 stop bits, 2=2 stop bits
this->stop_bits_ = (stop_bits == 0) ? 1 : (stop_bits == 1) ? 1 : 2;
// Convert CDC parity to UART parity format
// CDC: 0=None, 1=Odd, 2=Even, 3=Mark, 4=Space
switch (parity) {
case 0:
this->parity_ = uart::UART_CONFIG_PARITY_NONE;
break;
case 1:
this->parity_ = uart::UART_CONFIG_PARITY_ODD;
break;
case 2:
this->parity_ = uart::UART_CONFIG_PARITY_EVEN;
break;
default:
// Mark and Space parity are not commonly supported, default to None
this->parity_ = uart::UART_CONFIG_PARITY_NONE;
break;
}
// Invoke user callback in main loop context
if (this->line_coding_callback_ != nullptr) {
this->line_coding_callback_(bit_rate, stop_bits, parity, data_bits);
}
break;
}
}
// Return event to pool for reuse
this->event_pool_.release(event);
}
}
void USBCDCACMInstance::usb_tx_task_fn(void *arg) {
auto *instance = static_cast<USBCDCACMInstance *>(arg);
instance->usb_tx_task();
}
void USBCDCACMInstance::usb_tx_task() {
uint8_t data[CONFIG_TINYUSB_CDC_TX_BUFSIZE] = {0};
size_t tx_data_size = 0;
while (1) {
// Wait for a notification from the bridge component
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// When we do wake up, we can be sure there is data in the ring buffer
esp_err_t ret = ringbuf_read_bytes(this->usb_tx_ringbuf_, data, CONFIG_TINYUSB_CDC_TX_BUFSIZE, &tx_data_size, 0);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "USB TX itf=%d: RingBuf read failed", this->itf_);
continue;
} else if (tx_data_size == 0) {
ESP_LOGD(TAG, "USB TX itf=%d: RingBuf empty, skipping", this->itf_);
continue;
}
ESP_LOGV(TAG, "USB TX itf=%d: Read %d bytes from buffer", this->itf_, tx_data_size);
ESP_LOGVV(TAG, "data = %s", format_hex_pretty(data, tx_data_size).c_str());
// Serial data will be split up into 64 byte chunks to be sent over USB so this
// usually will take multiple iterations
uint8_t *data_head = &data[0];
while (tx_data_size > 0) {
size_t queued = tinyusb_cdcacm_write_queue(this->itf_, data_head, tx_data_size);
ESP_LOGV(TAG, "USB TX itf=%d: enqueued: size=%d, queued=%u", this->itf_, tx_data_size, queued);
tx_data_size -= queued;
data_head += queued;
ESP_LOGV(TAG, "USB TX itf=%d: waiting 10ms for flush", this->itf_);
esp_err_t flush_ret = tinyusb_cdcacm_write_flush(this->itf_, pdMS_TO_TICKS(10));
if (flush_ret != ESP_OK) {
ESP_LOGE(TAG, "USB TX itf=%d: flush failed", this->itf_);
tud_cdc_n_write_clear(this->itf_);
break;
}
}
}
}
//==============================================================================
// UARTComponent Interface Implementation
//==============================================================================
void USBCDCACMInstance::write_array(const uint8_t *data, size_t len) {
if (len == 0) {
return;
}
// Write data to TX ring buffer
BaseType_t send_res = xRingbufferSend(this->usb_tx_ringbuf_, data, len, 0);
if (send_res != pdTRUE) {
ESP_LOGW(TAG, "USB TX itf=%d: buffer full, %u bytes dropped", this->itf_, len);
return;
}
// Notify TX task that data is available
if (this->usb_tx_task_handle_ != nullptr) {
xTaskNotifyGive(this->usb_tx_task_handle_);
}
}
bool USBCDCACMInstance::peek_byte(uint8_t *data) {
if (this->has_peek_) {
*data = this->peek_buffer_;
return true;
}
if (this->read_byte(&this->peek_buffer_)) {
*data = this->peek_buffer_;
this->has_peek_ = true;
return true;
}
return false;
}
bool USBCDCACMInstance::read_array(uint8_t *data, size_t len) {
if (len == 0) {
return true;
}
size_t original_len = len;
size_t bytes_read = 0;
// First, use the peek buffer if available
if (this->has_peek_) {
data[0] = this->peek_buffer_;
this->has_peek_ = false;
bytes_read = 1;
data++;
if (--len == 0) { // Decrement len first, then check it...
return true; // No more to read
}
}
// Read remaining bytes from RX ring buffer
size_t rx_size = 0;
uint8_t *buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len));
if (buf == nullptr) {
return false;
}
memcpy(data, buf, rx_size);
vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf);
bytes_read += rx_size;
data += rx_size;
len -= rx_size;
if (len == 0) {
return true; // No more to read
}
// Buffer's data may wrap around, in which case we should perform another read
buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len));
if (buf == nullptr) {
return false;
}
memcpy(data, buf, rx_size);
vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf);
bytes_read += rx_size;
return bytes_read == original_len;
}
int USBCDCACMInstance::available() {
UBaseType_t waiting = 0;
if (this->usb_rx_ringbuf_ != nullptr) {
vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
}
return static_cast<int>(waiting) + (this->has_peek_ ? 1 : 0);
}
void USBCDCACMInstance::flush() {
// Wait for TX ring buffer to be empty
if (this->usb_tx_ringbuf_ == nullptr) {
return;
}
UBaseType_t waiting = 1;
while (waiting > 0) {
vRingbufferGetInfo(this->usb_tx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
if (waiting > 0) {
vTaskDelay(pdMS_TO_TICKS(1));
}
}
// Also wait for USB to finish transmitting
tinyusb_cdcacm_write_flush(this->itf_, pdMS_TO_TICKS(100));
}
//==============================================================================
// USBCDCACMComponent Implementation
//==============================================================================
USBCDCACMComponent::USBCDCACMComponent() { global_usb_cdc_component = this; }
void USBCDCACMComponent::setup() {
// Setup all registered interfaces
for (auto interface : this->interfaces_) {
if (interface != nullptr) {
interface->setup();
}
}
}
void USBCDCACMComponent::loop() {
// Call loop() on all registered interfaces to process events
for (auto interface : this->interfaces_) {
if (interface != nullptr) {
interface->loop();
}
}
}
void USBCDCACMComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"USB CDC-ACM:\n"
" Number of Interfaces: %d",
this->interfaces_[MAX_USB_CDC_INSTANCES - 1] != nullptr ? MAX_USB_CDC_INSTANCES : 1);
}
void USBCDCACMComponent::add_interface(USBCDCACMInstance *interface) {
uint8_t itf_num = static_cast<uint8_t>(interface->get_itf());
if (itf_num < MAX_USB_CDC_INSTANCES) {
this->interfaces_[itf_num] = interface;
} else {
ESP_LOGE(TAG, "Interface number must be less than %u", MAX_USB_CDC_INSTANCES);
}
}
USBCDCACMInstance *USBCDCACMComponent::get_interface_by_number(uint8_t itf) {
for (auto interface : this->interfaces_) {
if ((interface != nullptr) && (interface->get_itf() == static_cast<tinyusb_cdcacm_itf_t>(itf))) {
return interface;
}
}
return nullptr;
}
} // namespace esphome::usb_cdc_acm
#endif
@@ -0,0 +1,135 @@
#pragma once
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "esphome/core/component.h"
#include "esphome/core/event_pool.h"
#include "esphome/core/lock_free_queue.h"
#include "esphome/components/uart/uart_component.h"
#include <functional>
#include "freertos/ringbuf.h"
#include "tusb_cdc_acm.h"
namespace esphome::usb_cdc_acm {
static const uint8_t EVENT_QUEUE_SIZE = 12;
static const uint8_t MAX_USB_CDC_INSTANCES = 2;
// Callback types for line coding and line state changes
using LineCodingCallback = std::function<void(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, uint8_t data_bits)>;
using LineStateCallback = std::function<void(bool dtr, bool rts)>;
// Event types
enum CDCEventType : uint8_t {
CDC_EVENT_LINE_STATE_CHANGED,
CDC_EVENT_LINE_CODING_CHANGED,
};
// Event structure for the queue
struct CDCEvent {
CDCEventType type;
union {
struct {
bool dtr;
bool rts;
} line_state;
struct {
uint32_t bit_rate;
uint8_t stop_bits;
uint8_t parity;
uint8_t data_bits;
} line_coding;
} data;
// Required by EventPool - called before returning to pool
void release() {
// No dynamic memory to clean up, data is stored inline
}
};
// Forward declaration
class USBCDCACMComponent;
/// Represents a single CDC ACM interface instance
class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMComponent> {
public:
void set_interface_number(uint8_t itf) { this->itf_ = static_cast<tinyusb_cdcacm_itf_t>(itf); }
void setup();
void loop();
// Get the CDC port number for this instance
tinyusb_cdcacm_itf_t get_itf() const { return this->itf_; }
// Ring buffer accessors for bridge components
RingbufHandle_t get_tx_ringbuf() const { return this->usb_tx_ringbuf_; }
RingbufHandle_t get_rx_ringbuf() const { return this->usb_rx_ringbuf_; }
// Task handle accessor for notifying TX task
TaskHandle_t get_tx_task_handle() const { return this->usb_tx_task_handle_; }
// Callback registration for line coding and line state changes
void set_line_coding_callback(LineCodingCallback callback) { this->line_coding_callback_ = std::move(callback); }
void set_line_state_callback(LineStateCallback callback) { this->line_state_callback_ = std::move(callback); }
// Called from TinyUSB task context (SPSC producer) - queues event for processing in main loop
void queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, uint8_t data_bits);
void queue_line_state_event(bool dtr, bool rts);
static void usb_tx_task_fn(void *arg);
void usb_tx_task();
// UARTComponent interface implementation
void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
int available() override;
void flush() override;
protected:
void check_logger_conflict() override {}
// Process queued events and invoke callbacks (called from main loop)
void process_events_();
TaskHandle_t usb_tx_task_handle_{nullptr};
tinyusb_cdcacm_itf_t itf_{TINYUSB_CDC_ACM_0};
RingbufHandle_t usb_tx_ringbuf_{nullptr};
RingbufHandle_t usb_rx_ringbuf_{nullptr};
// User-registered callbacks (called from main loop)
LineCodingCallback line_coding_callback_{nullptr};
LineStateCallback line_state_callback_{nullptr};
// Lock-free queue and event pool for cross-task event passing
EventPool<CDCEvent, EVENT_QUEUE_SIZE> event_pool_;
LockFreeQueue<CDCEvent, EVENT_QUEUE_SIZE> event_queue_;
// RX buffer for peek functionality
uint8_t peek_buffer_{0};
bool has_peek_{false};
};
/// Main USB CDC ACM component that manages the USB device and all CDC interfaces
class USBCDCACMComponent : public Component {
public:
USBCDCACMComponent();
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::IO; }
// Interface management
void add_interface(USBCDCACMInstance *interface);
USBCDCACMInstance *get_interface_by_number(uint8_t itf);
protected:
std::array<USBCDCACMInstance *, MAX_USB_CDC_INSTANCES> interfaces_{nullptr, nullptr};
};
extern USBCDCACMComponent *global_usb_cdc_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome::usb_cdc_acm
#endif
+1 -1
View File
@@ -171,7 +171,7 @@ class DeferredUpdateEventSourceList : public std::list<DeferredUpdateEventSource
* by esphome.io by default), an event source under '/events' that automatically sends
* all state updates in real time + the debug log. Lastly, there's an REST API available
* under the '/light/...', '/sensor/...', ... URLs. A full documentation for this API
* can be found under https://esphome.io/web-api/index.html.
* can be found under https://esphome.io/web-api/.
*/
class WebServer : public Controller,
public Component,
@@ -190,9 +190,8 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
}
#endif
stream->print(
ESPHOME_F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
"REST API documentation.</p>"));
stream->print(ESPHOME_F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/\">ESPHome Web API</a> for "
"REST API documentation.</p>"));
#if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED)
// Show OTA form only if web_server OTA is not explicitly disabled
// Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
@@ -312,6 +312,23 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
char buf[33];
memcpy(buf, it.ssid, it.ssid_len);
buf[it.ssid_len] = '\0';
// LibreTiny can send spurious disconnect events with empty ssid/bssid during connection.
// These are typically "Association Leave" events that don't indicate actual failures:
// [W][wifi_lt]: Disconnected ssid='' bssid=00:00:00:00:00:00 reason='Association Leave'
// [W][wifi_lt]: Disconnected ssid='' bssid=00:00:00:00:00:00 reason='Association Leave'
// [V][wifi_lt]: Connected ssid='WIFI' bssid=... channel=3, authmode=WPA2 PSK
// Without this check, the spurious events set s_sta_connecting=false, causing
// wifi_sta_connect_status_() to return IDLE. The main loop then sees
// "Unknown connection status 0" (wifi_component.cpp check_connecting_finished)
// and calls retry_connect(), aborting a connection that may succeed moments later.
// Real connection failures will have ssid/bssid populated, or we'll hit the 30s timeout.
if (it.ssid_len == 0 && s_sta_connecting) {
ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s)",
get_disconnect_reason_str(it.reason));
break;
}
if (it.reason == WIFI_REASON_NO_AP_FOUND) {
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
} else {
@@ -7,16 +7,24 @@
#ifdef USE_WIFI
namespace esphome::wifi_signal {
#ifdef USE_WIFI_LISTENERS
class WiFiSignalSensor : public sensor::Sensor, public PollingComponent, public wifi::WiFiConnectStateListener {
#else
class WiFiSignalSensor : public sensor::Sensor, public PollingComponent {
#endif
public:
#ifdef USE_WIFI_LISTENERS
void setup() override { wifi::global_wifi_component->add_connect_state_listener(this); }
#endif
void update() override { this->publish_state(wifi::global_wifi_component->wifi_rssi()); }
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
#ifdef USE_WIFI_LISTENERS
// WiFiConnectStateListener interface - update RSSI immediately on connect
void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override { this->update(); }
#endif
};
} // namespace esphome::wifi_signal
+1
View File
@@ -559,6 +559,7 @@ CONF_LOGS = "logs"
CONF_LONGITUDE = "longitude"
CONF_LOOP_TIME = "loop_time"
CONF_LOW = "low"
CONF_LOW_POWER_MODE = "low_power_mode"
CONF_LOW_VOLTAGE_REFERENCE = "low_voltage_reference"
CONF_MAC_ADDRESS = "mac_address"
CONF_MAGNITUDE = "magnitude"
+14
View File
@@ -6,4 +6,18 @@ namespace esphome {
constinit const Color Color::BLACK(0, 0, 0, 0);
constinit const Color Color::WHITE(255, 255, 255, 255);
Color Color::gradient(const Color &to_color, uint8_t amnt) {
Color new_color;
float amnt_f = float(amnt) / 255.0f;
new_color.r = amnt_f * (to_color.r - this->r) + this->r;
new_color.g = amnt_f * (to_color.g - this->g) + this->g;
new_color.b = amnt_f * (to_color.b - this->b) + this->b;
new_color.w = amnt_f * (to_color.w - this->w) + this->w;
return new_color;
}
Color Color::fade_to_white(uint8_t amnt) { return this->gradient(Color::WHITE, amnt); }
Color Color::fade_to_black(uint8_t amnt) { return this->gradient(Color::BLACK, amnt); }
} // namespace esphome
+3 -11
View File
@@ -174,17 +174,9 @@ struct Color {
uint8_t((uint16_t(b) * 255U / max_rgb)), w);
}
Color gradient(const Color &to_color, uint8_t amnt) {
Color new_color;
float amnt_f = float(amnt) / 255.0f;
new_color.r = amnt_f * (to_color.r - (*this).r) + (*this).r;
new_color.g = amnt_f * (to_color.g - (*this).g) + (*this).g;
new_color.b = amnt_f * (to_color.b - (*this).b) + (*this).b;
new_color.w = amnt_f * (to_color.w - (*this).w) + (*this).w;
return new_color;
}
Color fade_to_white(uint8_t amnt) { return (*this).gradient(Color::WHITE, amnt); }
Color fade_to_black(uint8_t amnt) { return (*this).gradient(Color::BLACK, amnt); }
Color gradient(const Color &to_color, uint8_t amnt);
Color fade_to_white(uint8_t amnt);
Color fade_to_black(uint8_t amnt);
Color lighten(uint8_t delta) { return *this + delta; }
Color darken(uint8_t delta) { return *this - delta; }
+1 -1
View File
@@ -87,7 +87,7 @@ def validate_hostname(config):
_LOGGER.warning(
"'%s': Using the '_' (underscore) character in the hostname is discouraged "
"as it can cause problems with some DHCP and local name services. "
"For more information, see https://esphome.io/guides/faq.html#why-shouldn-t-i-use-underscores-in-my-device-name",
"For more information, see https://esphome.io/guides/faq/#why-shouldnt-i-use-underscores-in-my-device-name",
config[CONF_NAME],
)
return config
+5
View File
@@ -134,6 +134,11 @@ inline std::string operator+(const StringRef &lhs, const std::string &rhs) {
return str;
}
inline std::string operator+(const std::string &lhs, const StringRef &rhs) {
std::string str(lhs);
str.append(rhs.c_str(), rhs.size());
return str;
}
#ifdef USE_JSON
// NOLINTNEXTLINE(readability-identifier-naming)
inline void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); }
+1 -1
View File
@@ -402,7 +402,7 @@ def run_ota_impl_(
)
_LOGGER.error(
"(If this error persists, please set a static IP address: "
"https://esphome.io/components/wifi.html#manual-ips)"
"https://esphome.io/components/wifi/#manual-ips)"
)
raise OTAError(err) from err
+1 -1
View File
@@ -192,7 +192,7 @@ def get_esphome_device_ip(
data = json.loads(payload)
if "name" not in data or data["name"] != dev_name:
_LOGGER.Warn("Wrong device answer")
_LOGGER.warning("Wrong device answer")
return
dev_ip = []
+1 -1
View File
@@ -274,7 +274,7 @@ def check_strapping_pin(conf, strapping_pin_list: set[int], logger: Logger):
logger.warning(
f"GPIO{num} is a strapping PIN and should only be used for I/O with care.\n"
"Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n"
"See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins",
"See https://esphome.io/guides/faq/#why-am-i-getting-a-warning-about-strapping-pins",
)
# mitigate undisciplined use of strapping:
if num not in strapping_pin_list and conf.get(CONF_IGNORE_STRAPPING_WARNING):
+1 -1
View File
@@ -375,6 +375,6 @@ def get_esp32_arduino_flash_error_help() -> str | None:
+ "For detailed migration instructions, see:\n"
+ color(
AnsiFore.BLUE,
"https://esphome.io/guides/esp32_arduino_to_idf.html\n\n",
"https://esphome.io/guides/esp32_arduino_to_idf/\n\n",
)
)
+2 -4
View File
@@ -411,9 +411,7 @@ def wizard(path: Path) -> int:
"https://docs.platformio.org/en/latest/platforms/espressif8266.html#boards"
)
elif platform == "RP2040":
board_link = (
"https://www.raspberrypi.com/documentation/microcontrollers/rp2040.html"
)
board_link = "https://www.raspberrypi.com/documentation/microcontrollers/silicon.html#rp2040"
elif platform in ["BK72XX", "LN882X", "RTL87XX"]:
board_link = "https://docs.libretiny.eu/docs/status/supported/"
else:
@@ -555,7 +553,7 @@ def wizard(path: Path) -> int:
safe_print("Next steps:")
safe_print(" > Follow the rest of the getting started guide:")
safe_print(
" > https://esphome.io/guides/getting_started_command_line.html#adding-some-features"
" > https://esphome.io/guides/getting_started_command_line/#adding-some-features"
)
safe_print(" > to learn how to customize ESPHome and install it to your device.")
return 0
+12 -14
View File
@@ -32,20 +32,24 @@ build_flags =
; This are common settings for all environments.
[common]
lib_deps =
esphome/noise-c@0.1.10 ; api
improv/Improv@1.2.4 ; improv_serial / esp32_improv
; Base dependencies for all environments
lib_deps_base =
bblanchon/ArduinoJson@7.4.2 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
pavlodn/HaierProtocol@0.9.31 ; haier
kikuchan98/pngle@1.1.0 ; online_image
https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps
; 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
lvgl/lvgl@8.4.0 ; lvgl
lib_deps =
${common.lib_deps_base}
esphome/noise-c@0.1.10 ; api
improv/Improv@1.2.4 ; improv_serial / esp32_improv
kikuchan98/pngle@1.1.0 ; online_image
; Using the repository directly, otherwise ESP-IDF can't use the library
https://github.com/bitbank2/JPEGDEC.git#ca1e0f2 ; online_image
; 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
lvgl/lvgl@8.4.0 ; lvgl
; This dependency is used only in unit tests.
; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py
; See scripts/cpp_unit_test.py and tests/components/README.md
@@ -236,13 +240,7 @@ build_flags =
-DUSE_ZEPHYR
-DUSE_NRF52
lib_deps =
bblanchon/ArduinoJson@7.4.2 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code
pavlodn/HaierProtocol@0.9.31 ; haier
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
lvgl/lvgl@8.4.0 ; lvgl
${common.lib_deps_base}
; All the actual environments are defined below.
+1 -1
View File
@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0
click==8.1.7
esphome-dashboard==20251013.0
aioesphomeapi==43.1.0
aioesphomeapi==43.2.1
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.18.16 # dashboard_import
@@ -176,10 +176,7 @@ def test_single_package(
assert actual == expected
assert (
"Including a single package under `packages:` is deprecated. Use a list instead."
in caplog.text
)
assert "This method for including packages will go away in 2026.7.0" in caplog.text
def test_package_append(basic_wifi, basic_esphome):
+17
View File
@@ -0,0 +1,17 @@
sensor:
- platform: hlw8032
voltage:
name: HLW8032 Voltage
id: hlw8032_voltage
current:
name: HLW8032 Current
id: hlw8032_current
power:
name: HLW8032 Power
id: hlw8032_power
apparent_power:
name: HLW8032 Apparent Power
id: hlw8032_apparent_power
power_factor:
name: HLW8032 Power Factor
id: hlw8032_power_factor
@@ -0,0 +1,4 @@
packages:
uart_4800_even: !include ../../test_build_components/common/uart_4800_even/esp32-idf.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
uart_4800_even: !include ../../test_build_components/common/uart_4800_even/esp8266-ard.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
uart_4800_even: !include ../../test_build_components/common/uart_4800_even/rp2040-ard.yaml
<<: !include common.yaml
+4 -4
View File
@@ -25,15 +25,15 @@ display:
oe_pin: GPIO15
clk_pin: GPIO16
pages:
- id: page1
- id: page1_hub75
lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height());
- id: page2
- id: page2_hub75
lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height());
on_page_change:
from: page1
to: page2
from: page1_hub75
to: page2_hub75
then:
lambda: |-
ESP_LOGD("display", "1 -> 2");
+1 -1
View File
@@ -5,7 +5,7 @@ button:
- platform: micronova
custom_button:
name: Custom Micronova Button
memory_location: 0xA0
memory_location: 0x20
memory_address: 0x7D
memory_data: 0x0F
+1
View File
@@ -2,6 +2,7 @@ pca9685:
i2c_id: i2c_bus
frequency: 500
address: 0x0
phase_balancer: linear
output:
- platform: pca9685
+1 -1
View File
@@ -16,4 +16,4 @@ display:
qr_code:
- id: qr_code_homepage_qr
value: https://esphome.io/index.html
value: https://esphome.io/
@@ -0,0 +1,5 @@
<<: !include tinyusb_common.yaml
usb_cdc_acm:
interfaces:
id: usb_cdc_acm1
@@ -0,0 +1,5 @@
<<: !include tinyusb_common.yaml
usb_cdc_acm:
interfaces:
- id: usb_cdc_acm1
@@ -0,0 +1,6 @@
<<: !include tinyusb_common.yaml
usb_cdc_acm:
interfaces:
- id: usb_cdc_acm1
- id: usb_cdc_acm2
@@ -0,0 +1,8 @@
tinyusb:
id: tinyusb_test
usb_lang_id: 0x0123
usb_manufacturer_str: ESPHomeTestManufacturer
usb_product_id: 0x1234
usb_product_str: ESPHomeTestProduct
usb_serial_str: ESPHomeTestSerialNumber
usb_vendor_id: 0x2345