mirror of
https://github.com/esphome/esphome.git
synced 2026-07-01 13:02:50 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f48106b160 | |||
| 1b5bf2c848 | |||
| c4fa476c3c | |||
| c4869bad88 | |||
| dc56cd1d1f | |||
| d2a819eb77 | |||
| 0ac61cbb9b | |||
| c9c99a22e0 | |||
| 91250fd46c | |||
| 641914cdbe | |||
| 840859ab7c | |||
| 97b712da98 | |||
| b5c36140fa | |||
| 48a9c1cd67 | |||
| f7843582e8 | |||
| 2c749e9dbe | |||
| 8479664df1 | |||
| 5a1d6428b2 | |||
| a39be5a461 | |||
| da930310b1 | |||
| af296eb600 | |||
| 2c11c65faf | |||
| 29d890bb0f | |||
| efa39ae591 | |||
| 4b57ac3236 | |||
| 997f825cd3 | |||
| 27fe866d5e | |||
| c5c6ce6b0e | |||
| 15e2a778d4 | |||
| 1f5a35a99f | |||
| 0975755a9d | |||
| 19f4845185 |
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.2.1
|
||||
PROJECT_NUMBER = 2026.2.4
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -944,12 +944,6 @@ def command_clean_all(args: ArgsProtocol) -> int | None:
|
||||
return 0
|
||||
|
||||
|
||||
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome import mqtt
|
||||
|
||||
return mqtt.get_fingerprint(config)
|
||||
|
||||
|
||||
def command_version(args: ArgsProtocol) -> int | None:
|
||||
safe_print(f"Version: {const.__version__}")
|
||||
return 0
|
||||
@@ -1237,7 +1231,6 @@ POST_CONFIG_ACTIONS = {
|
||||
"run": command_run,
|
||||
"clean": command_clean,
|
||||
"clean-mqtt": command_clean_mqtt,
|
||||
"mqtt-fingerprint": command_mqtt_fingerprint,
|
||||
"idedata": command_idedata,
|
||||
"rename": command_rename,
|
||||
"discover": command_discover,
|
||||
@@ -1451,13 +1444,6 @@ def parse_args(argv):
|
||||
)
|
||||
parser_wizard.add_argument("configuration", help="Your YAML configuration file.")
|
||||
|
||||
parser_fingerprint = subparsers.add_parser(
|
||||
"mqtt-fingerprint", help="Get the SSL fingerprint from a MQTT broker."
|
||||
)
|
||||
parser_fingerprint.add_argument(
|
||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||
)
|
||||
|
||||
subparsers.add_parser("version", help="Print the ESPHome version and exit.")
|
||||
|
||||
parser_clean = subparsers.add_parser(
|
||||
|
||||
@@ -1334,9 +1334,8 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne
|
||||
resp.target_temperature_low = wh->get_target_temperature_low();
|
||||
resp.target_temperature_high = wh->get_target_temperature_high();
|
||||
resp.state = wh->get_state();
|
||||
resp.key = wh->get_object_id_hash();
|
||||
|
||||
return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
return fill_and_encode_entity_state(wh, resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
|
||||
auto *wh = static_cast<water_heater::WaterHeater *>(entity);
|
||||
|
||||
@@ -36,6 +36,8 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
||||
static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str()
|
||||
static std::string value_to_string(const std::string &val) { return val; }
|
||||
static std::string value_to_string(std::string &&val) { return std::move(val); }
|
||||
static std::string value_to_string(const StringRef &val) { return val.str(); }
|
||||
static std::string value_to_string(StringRef &&val) { return val.str(); }
|
||||
|
||||
public:
|
||||
TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
|
||||
|
||||
@@ -550,21 +550,8 @@ def binary_sensor_schema(
|
||||
return _BINARY_SENSOR_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
async def setup_binary_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "binary_sensor")
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get(
|
||||
CONF_PUBLISH_INITIAL_STATE, False
|
||||
)
|
||||
cg.add(var.set_trigger_on_initial_state(trigger))
|
||||
if inverted := config.get(CONF_INVERTED):
|
||||
cg.add(var.set_inverted(inverted))
|
||||
if filters_config := config.get(CONF_FILTERS):
|
||||
filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config)
|
||||
cg.add(var.add_filters(filters))
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_binary_sensor_automations(var, config):
|
||||
for conf in config.get(CONF_ON_PRESS, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
@@ -616,6 +603,25 @@ async def setup_binary_sensor_core_(var, config):
|
||||
conf,
|
||||
)
|
||||
|
||||
|
||||
async def setup_binary_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "binary_sensor")
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get(
|
||||
CONF_PUBLISH_INITIAL_STATE, False
|
||||
)
|
||||
cg.add(var.set_trigger_on_initial_state(trigger))
|
||||
if inverted := config.get(CONF_INVERTED):
|
||||
cg.add(var.set_inverted(inverted))
|
||||
if filters_config := config.get(CONF_FILTERS):
|
||||
cg.add_define("USE_BINARY_SENSOR_FILTER")
|
||||
filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config)
|
||||
cg.add(var.add_filters(filters))
|
||||
|
||||
CORE.add_job(_build_binary_sensor_automations, var, config)
|
||||
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
|
||||
@@ -178,8 +178,11 @@ async def to_code_base(config):
|
||||
bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs)))
|
||||
|
||||
# Although this component does not use SPI, the BSEC2 Arduino library requires the SPI library
|
||||
# The BSEC2 and BME68x Arduino libraries unconditionally include Wire.h and
|
||||
# SPI.h in their source files, so these libraries must be available even though
|
||||
# ESPHome uses its own I2C/SPI abstractions instead of the Arduino ones.
|
||||
if core.CORE.using_arduino:
|
||||
cg.add_library("Wire", None)
|
||||
cg.add_library("SPI", None)
|
||||
cg.add_library(
|
||||
"BME68x Sensor library",
|
||||
|
||||
@@ -242,6 +242,9 @@ void CC1101Component::begin_tx() {
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
}
|
||||
// Transition through IDLE to bypass CCA (Clear Channel Assessment) which can
|
||||
// block TX entry when strobing from RX, and to ensure FS_AUTOCAL calibration
|
||||
this->enter_idle_();
|
||||
if (!this->enter_tx_()) {
|
||||
ESP_LOGW(TAG, "Failed to enter TX state!");
|
||||
}
|
||||
@@ -252,6 +255,8 @@ void CC1101Component::begin_rx() {
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
}
|
||||
// Transition through IDLE to ensure FS_AUTOCAL calibration occurs
|
||||
this->enter_idle_();
|
||||
if (!this->enter_rx_()) {
|
||||
ESP_LOGW(TAG, "Failed to enter RX state!");
|
||||
}
|
||||
|
||||
@@ -64,6 +64,9 @@ class Dsmr : public Component, public uart::UARTDevice {
|
||||
void dump_config() override;
|
||||
|
||||
void set_decryption_key(const char *decryption_key);
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("Pass .c_str() - e.g. set_decryption_key(key.c_str()). Removed in 2026.8.0", "2026.2.0")
|
||||
void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key(decryption_key.c_str()); }
|
||||
void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; }
|
||||
void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; }
|
||||
void set_request_interval(uint32_t interval) { this->request_interval_ = interval; }
|
||||
|
||||
@@ -29,10 +29,10 @@ enum class CleaningState : uint8_t {
|
||||
enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER };
|
||||
|
||||
struct HonSettings {
|
||||
hon_protocol::VerticalSwingMode last_vertiacal_swing;
|
||||
hon_protocol::HorizontalSwingMode last_horizontal_swing;
|
||||
bool beeper_state;
|
||||
bool quiet_mode_state;
|
||||
hon_protocol::VerticalSwingMode last_vertiacal_swing{hon_protocol::VerticalSwingMode::CENTER};
|
||||
hon_protocol::HorizontalSwingMode last_horizontal_swing{hon_protocol::HorizontalSwingMode::CENTER};
|
||||
bool beeper_state{true};
|
||||
bool quiet_mode_state{false};
|
||||
};
|
||||
|
||||
class HonClimate : public HaierClimateBase {
|
||||
@@ -189,7 +189,7 @@ class HonClimate : public HaierClimateBase {
|
||||
int big_data_sensors_{0};
|
||||
esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{};
|
||||
esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{};
|
||||
HonSettings settings_;
|
||||
HonSettings settings_{};
|
||||
ESPPreferenceObject hon_rtc_;
|
||||
SwitchState quiet_mode_state_{SwitchState::OFF};
|
||||
};
|
||||
|
||||
@@ -95,7 +95,7 @@ void HMC5883LComponent::update() {
|
||||
float mg_per_bit;
|
||||
switch (this->range_) {
|
||||
case HMC5883L_RANGE_88_UT:
|
||||
mg_per_bit = 0.073f;
|
||||
mg_per_bit = 0.73f;
|
||||
break;
|
||||
case HMC5883L_RANGE_130_UT:
|
||||
mg_per_bit = 0.92f;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "ota_http_request.h"
|
||||
|
||||
#include <cctype>
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -210,6 +212,26 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
return ota::OTA_RESPONSE_OK;
|
||||
}
|
||||
|
||||
// URL-encode characters that are not unreserved per RFC 3986 section 2.3.
|
||||
// This is needed for embedding userinfo (username/password) in URLs safely.
|
||||
static std::string url_encode(const std::string &str) {
|
||||
std::string result;
|
||||
result.reserve(str.size());
|
||||
for (char c : str) {
|
||||
if (std::isalnum(static_cast<unsigned char>(c)) || c == '-' || c == '_' || c == '.' || c == '~') {
|
||||
result += c;
|
||||
} else {
|
||||
result += '%';
|
||||
result += format_hex_pretty_char((static_cast<uint8_t>(c) >> 4) & 0x0F);
|
||||
result += format_hex_pretty_char(static_cast<uint8_t>(c) & 0x0F);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void OtaHttpRequestComponent::set_password(const std::string &password) { this->password_ = url_encode(password); }
|
||||
void OtaHttpRequestComponent::set_username(const std::string &username) { this->username_ = url_encode(username); }
|
||||
|
||||
std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) {
|
||||
if (this->username_.empty() || this->password_.empty()) {
|
||||
return url;
|
||||
|
||||
@@ -29,9 +29,9 @@ class OtaHttpRequestComponent : public ota::OTAComponent, public Parented<HttpRe
|
||||
|
||||
void set_md5_url(const std::string &md5_url);
|
||||
void set_md5(const std::string &md5) { this->md5_expected_ = md5; }
|
||||
void set_password(const std::string &password) { this->password_ = password; }
|
||||
void set_password(const std::string &password);
|
||||
void set_url(const std::string &url);
|
||||
void set_username(const std::string &username) { this->username_ = username; }
|
||||
void set_username(const std::string &username);
|
||||
|
||||
std::string md5_computed() { return this->md5_computed_; }
|
||||
std::string md5_expected() { return this->md5_expected_; }
|
||||
|
||||
@@ -43,3 +43,4 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await improv_base.setup_improv_core(var, config, "improv_serial")
|
||||
cg.add_define("USE_IMPROV_SERIAL")
|
||||
|
||||
@@ -590,7 +590,7 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
|
||||
for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT
|
||||
((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE));
|
||||
index += CMD_REG_DATA_REPLY_SIZE) {
|
||||
memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], sizeof(CMD_REG_DATA_REPLY_SIZE));
|
||||
memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], CMD_REG_DATA_REPLY_SIZE);
|
||||
byteswap(this->cmd_reply_.data[reg_element]);
|
||||
reg_element++;
|
||||
}
|
||||
@@ -729,9 +729,9 @@ void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) {
|
||||
cmd_frame.data_length = 0;
|
||||
cmd_frame.header = CMD_FRAME_HEADER;
|
||||
cmd_frame.command = CMD_WRITE_REGISTER;
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], ®, sizeof(CMD_REG_DATA_REPLY_SIZE));
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], ®, CMD_REG_DATA_REPLY_SIZE);
|
||||
cmd_frame.data_length += 2;
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &value, sizeof(CMD_REG_DATA_REPLY_SIZE));
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &value, CMD_REG_DATA_REPLY_SIZE);
|
||||
cmd_frame.data_length += 2;
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value);
|
||||
|
||||
@@ -133,12 +133,12 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"max7129digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA
|
||||
"max7219digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action(
|
||||
"max7129digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA
|
||||
"max7219digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA
|
||||
)
|
||||
async def max7129digit_invert_to_code(config, action_id, template_arg, args):
|
||||
async def max7219digit_invert_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
cg.add(var.set_state(config[CONF_STATE]))
|
||||
@@ -146,12 +146,12 @@ async def max7129digit_invert_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"max7129digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA
|
||||
"max7219digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action(
|
||||
"max7129digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA
|
||||
"max7219digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA
|
||||
)
|
||||
async def max7129digit_visible_to_code(config, action_id, template_arg, args):
|
||||
async def max7219digit_visible_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
cg.add(var.set_state(config[CONF_STATE]))
|
||||
@@ -159,12 +159,12 @@ async def max7129digit_visible_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"max7129digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA
|
||||
"max7219digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action(
|
||||
"max7129digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA
|
||||
"max7219digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA
|
||||
)
|
||||
async def max7129digit_reverse_to_code(config, action_id, template_arg, args):
|
||||
async def max7219digit_reverse_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
cg.add(var.set_state(config[CONF_STATE]))
|
||||
@@ -183,9 +183,9 @@ MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value(
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"max7129digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA
|
||||
"max7219digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA
|
||||
)
|
||||
async def max7129digit_intensity_to_code(config, action_id, template_arg, args):
|
||||
async def max7219digit_intensity_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_INTENSITY], args, cg.uint8)
|
||||
|
||||
@@ -39,6 +39,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_COLOR_ORDER,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_DISABLED,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_ID,
|
||||
CONF_INIT_SEQUENCE,
|
||||
@@ -87,38 +88,27 @@ COLOR_DEPTHS = {
|
||||
|
||||
def model_schema(config):
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
transform = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
||||
}
|
||||
)
|
||||
if model.get_default(CONF_SWAP_XY) != cv.UNDEFINED:
|
||||
transform = transform.extend(
|
||||
model.defaults[CONF_SWAP_XY] = cv.UNDEFINED
|
||||
transform = cv.Any(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
||||
cv.Optional(CONF_SWAP_XY): cv.invalid(
|
||||
"Axis swapping not supported by this model"
|
||||
)
|
||||
"Axis swapping not supported by DSI displays"
|
||||
),
|
||||
}
|
||||
)
|
||||
else:
|
||||
transform = transform.extend(
|
||||
{
|
||||
cv.Required(CONF_SWAP_XY): cv.boolean,
|
||||
}
|
||||
)
|
||||
),
|
||||
cv.one_of(CONF_DISABLED, lower=True),
|
||||
)
|
||||
# CUSTOM model will need to provide a custom init sequence
|
||||
iseqconf = (
|
||||
cv.Required(CONF_INIT_SEQUENCE)
|
||||
if model.initsequence is None
|
||||
else cv.Optional(CONF_INIT_SEQUENCE)
|
||||
)
|
||||
swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False)
|
||||
|
||||
# Dimensions are optional if the model has a default width and the swap_xy transform is not overridden
|
||||
cv_dimensions = (
|
||||
cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required
|
||||
)
|
||||
# Dimensions are optional if the model has a default width
|
||||
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
|
||||
pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_24BIT, "16", "24")
|
||||
schema = display.FULL_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
@@ -213,9 +203,9 @@ async def to_code(config):
|
||||
cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH]))
|
||||
cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH]))
|
||||
cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH]))
|
||||
cg.add(var.set_pclk_frequency(int(config[CONF_PCLK_FREQUENCY] / 1e6)))
|
||||
cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY] / 1.0e6))
|
||||
cg.add(var.set_lanes(int(config[CONF_LANES])))
|
||||
cg.add(var.set_lane_bit_rate(int(config[CONF_LANE_BIT_RATE] / 1e6)))
|
||||
cg.add(var.set_lane_bit_rate(config[CONF_LANE_BIT_RATE] / 1.0e6))
|
||||
if reset_pin := config.get(CONF_RESET_PIN):
|
||||
reset = await cg.gpio_pin_expression(reset_pin)
|
||||
cg.add(var.set_reset_pin(reset))
|
||||
|
||||
@@ -374,7 +374,7 @@ void MIPI_DSI::dump_config() {
|
||||
"\n Swap X/Y: %s"
|
||||
"\n Rotation: %d degrees"
|
||||
"\n DSI Lanes: %u"
|
||||
"\n Lane Bit Rate: %uMbps"
|
||||
"\n Lane Bit Rate: %.0fMbps"
|
||||
"\n HSync Pulse Width: %u"
|
||||
"\n HSync Back Porch: %u"
|
||||
"\n HSync Front Porch: %u"
|
||||
@@ -385,7 +385,7 @@ void MIPI_DSI::dump_config() {
|
||||
"\n Display Pixel Mode: %d bit"
|
||||
"\n Color Order: %s"
|
||||
"\n Invert Colors: %s"
|
||||
"\n Pixel Clock: %dMHz",
|
||||
"\n Pixel Clock: %.1fMHz",
|
||||
this->model_, this->width_, this->height_, YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)),
|
||||
YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY)), YESNO(this->madctl_ & MADCTL_MV), this->rotation_,
|
||||
this->lanes_, this->lane_bit_rate_, this->hsync_pulse_width_, this->hsync_back_porch_,
|
||||
|
||||
@@ -47,7 +47,7 @@ class MIPI_DSI : public display::Display {
|
||||
|
||||
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); }
|
||||
void set_pclk_frequency(uint32_t pclk_frequency) { this->pclk_frequency_ = pclk_frequency; }
|
||||
void set_pclk_frequency(float pclk_frequency) { this->pclk_frequency_ = pclk_frequency; }
|
||||
int get_width_internal() override { return this->width_; }
|
||||
int get_height_internal() override { return this->height_; }
|
||||
void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; }
|
||||
@@ -58,7 +58,7 @@ class MIPI_DSI : public display::Display {
|
||||
void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; }
|
||||
void set_init_sequence(const std::vector<uint8_t> &init_sequence) { this->init_sequence_ = init_sequence; }
|
||||
void set_model(const char *model) { this->model_ = model; }
|
||||
void set_lane_bit_rate(uint16_t lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; }
|
||||
void set_lane_bit_rate(float lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; }
|
||||
void set_lanes(uint8_t lanes) { this->lanes_ = lanes; }
|
||||
void set_madctl(uint8_t madctl) { this->madctl_ = madctl; }
|
||||
|
||||
@@ -95,9 +95,9 @@ class MIPI_DSI : public display::Display {
|
||||
uint16_t vsync_front_porch_ = 10;
|
||||
const char *model_{"Unknown"};
|
||||
std::vector<uint8_t> init_sequence_{};
|
||||
uint16_t pclk_frequency_ = 16; // in MHz
|
||||
uint16_t lane_bit_rate_{1500}; // in Mbps
|
||||
uint8_t lanes_{2}; // 1, 2, 3 or 4 lanes
|
||||
float pclk_frequency_ = 16; // in MHz
|
||||
float lane_bit_rate_{1500}; // in Mbps
|
||||
uint8_t lanes_{2}; // 1, 2, 3 or 4 lanes
|
||||
|
||||
bool invert_colors_{};
|
||||
display::ColorOrder color_mode_{display::COLOR_ORDER_BGR};
|
||||
|
||||
@@ -90,8 +90,6 @@ DriverChip(
|
||||
(0xE9, 0xC8, 0x10, 0x0A, 0x00, 0x00, 0x80, 0x81, 0x12, 0x31, 0x23, 0x4F, 0x86, 0xA0, 0x00, 0x47, 0x08, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x98, 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x98, 0x13, 0x8B, 0xAF, 0x57, 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
|
||||
(0xEA, 0x97, 0x0C, 0x09, 0x09, 0x09, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x31, 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x9F, 0x20, 0x8B, 0xA8, 0x20, 0x64, 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x02, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x81, 0x00, 0x00, 0x00, 0x00),
|
||||
(0xEF, 0xFF, 0xFF, 0x01),
|
||||
(0x11, 0x00),
|
||||
(0x29, 0x00),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -109,6 +107,7 @@ DriverChip(
|
||||
lane_bit_rate="900Mbps",
|
||||
no_transform=True,
|
||||
color_order="RGB",
|
||||
reset_pin=33,
|
||||
initsequence=[
|
||||
(0x80, 0x8B),
|
||||
(0x81, 0x78),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import re
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
@@ -46,7 +44,6 @@ from esphome.const import (
|
||||
CONF_RETAIN,
|
||||
CONF_SHUTDOWN_MESSAGE,
|
||||
CONF_SKIP_CERT_CN_CHECK,
|
||||
CONF_SSL_FINGERPRINTS,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_SUBSCRIBE_QOS,
|
||||
CONF_TOPIC,
|
||||
@@ -221,13 +218,6 @@ def validate_config(value):
|
||||
return out
|
||||
|
||||
|
||||
def validate_fingerprint(value):
|
||||
value = cv.string(value)
|
||||
if re.match(r"^[0-9a-f]{40}$", value) is None:
|
||||
raise cv.Invalid("fingerprint must be valid SHA1 hash")
|
||||
return value
|
||||
|
||||
|
||||
def _consume_mqtt_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for MQTT component."""
|
||||
# MQTT needs 1 socket for the broker connection
|
||||
@@ -291,9 +281,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
validate_message_just_topic,
|
||||
),
|
||||
cv.Optional(CONF_SSL_FINGERPRINTS): cv.All(
|
||||
cv.only_on_esp8266, cv.ensure_list(validate_fingerprint)
|
||||
),
|
||||
cv.Optional(CONF_KEEPALIVE, default="15s"): cv.positive_time_period_seconds,
|
||||
cv.Optional(
|
||||
CONF_REBOOT_TIMEOUT, default="15min"
|
||||
@@ -444,14 +431,6 @@ async def to_code(config):
|
||||
if CONF_LEVEL in log_topic:
|
||||
cg.add(var.set_log_level(logger.LOG_LEVELS[log_topic[CONF_LEVEL]]))
|
||||
|
||||
if CONF_SSL_FINGERPRINTS in config:
|
||||
for fingerprint in config[CONF_SSL_FINGERPRINTS]:
|
||||
arr = [
|
||||
cg.RawExpression(f"0x{fingerprint[i : i + 2]}") for i in range(0, 40, 2)
|
||||
]
|
||||
cg.add(var.add_ssl_fingerprint(arr))
|
||||
cg.add_build_flag("-DASYNC_TCP_SSL_ENABLED=1")
|
||||
|
||||
cg.add(var.set_keep_alive(config[CONF_KEEPALIVE]))
|
||||
|
||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||
|
||||
@@ -21,11 +21,6 @@ class MQTTBackendESP8266 final : public MQTTBackend {
|
||||
}
|
||||
void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(ip, port); }
|
||||
void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); }
|
||||
#if ASYNC_TCP_SSL_ENABLED
|
||||
void set_secure(bool secure) { mqtt_client.setSecure(secure); }
|
||||
void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); }
|
||||
#endif
|
||||
|
||||
void set_on_connect(std::function<on_connect_callback_t> &&callback) final {
|
||||
this->mqtt_client_.onConnect(std::move(callback));
|
||||
}
|
||||
|
||||
@@ -21,11 +21,6 @@ class MQTTBackendLibreTiny final : public MQTTBackend {
|
||||
}
|
||||
void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(IPAddress(ip), port); }
|
||||
void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); }
|
||||
#if ASYNC_TCP_SSL_ENABLED
|
||||
void set_secure(bool secure) { mqtt_client.setSecure(secure); }
|
||||
void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); }
|
||||
#endif
|
||||
|
||||
void set_on_connect(std::function<on_connect_callback_t> &&callback) final {
|
||||
this->mqtt_client_.onConnect(std::move(callback));
|
||||
}
|
||||
|
||||
@@ -746,13 +746,6 @@ void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&call
|
||||
this->on_disconnect_.add(std::move(callback_copy));
|
||||
}
|
||||
|
||||
#if ASYNC_TCP_SSL_ENABLED
|
||||
void MQTTClientComponent::add_ssl_fingerprint(const std::array<uint8_t, SHA1_SIZE> &fingerprint) {
|
||||
this->mqtt_backend_.setSecure(true);
|
||||
this->mqtt_backend_.addServerFingerprint(fingerprint.data());
|
||||
}
|
||||
#endif
|
||||
|
||||
MQTTClientComponent *global_mqtt_client = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
// MQTTMessageTrigger
|
||||
|
||||
@@ -142,21 +142,6 @@ class MQTTClientComponent : public Component
|
||||
bool is_discovery_enabled() const;
|
||||
bool is_discovery_ip_enabled() const;
|
||||
|
||||
#if ASYNC_TCP_SSL_ENABLED
|
||||
/** Add a SSL fingerprint to use for TCP SSL connections to the MQTT broker.
|
||||
*
|
||||
* To use this feature you first have to globally enable the `ASYNC_TCP_SSL_ENABLED` define flag.
|
||||
* This function can be called multiple times and any certificate that matches any of the provided fingerprints
|
||||
* will match. Calling this method will also automatically disable all non-ssl connections.
|
||||
*
|
||||
* @warning This is *not* secure and *not* how SSL is usually done. You'll have to add
|
||||
* a separate fingerprint for every certificate you use. Additionally, the hashing
|
||||
* algorithm used here due to the constraints of the MCU, SHA1, is known to be insecure.
|
||||
*
|
||||
* @param fingerprint The SSL fingerprint as a 20 value long std::array.
|
||||
*/
|
||||
void add_ssl_fingerprint(const std::array<uint8_t, SHA1_SIZE> &fingerprint);
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); }
|
||||
void set_cl_certificate(const char *cert) { this->mqtt_backend_.set_cl_certificate(cert); }
|
||||
|
||||
@@ -61,7 +61,9 @@ struct IPAddress {
|
||||
IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); }
|
||||
IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; }
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0")
|
||||
ESPDEPRECATED(
|
||||
"str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0",
|
||||
"2026.2.0")
|
||||
std::string str() const {
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
this->str_to(buf);
|
||||
@@ -150,7 +152,9 @@ struct IPAddress {
|
||||
bool is_ip6() const { return IP_IS_V6(&ip_addr_); }
|
||||
bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); }
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0")
|
||||
ESPDEPRECATED(
|
||||
"str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0",
|
||||
"2026.2.0")
|
||||
std::string str() const {
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
this->str_to(buf);
|
||||
|
||||
@@ -240,6 +240,23 @@ def number_schema(
|
||||
return _NUMBER_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_number_automations(var, config):
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
for conf in config.get(CONF_ON_VALUE_RANGE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await cg.register_component(trigger, conf)
|
||||
if CONF_ABOVE in conf:
|
||||
template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float)
|
||||
cg.add(trigger.set_min(template_))
|
||||
if CONF_BELOW in conf:
|
||||
template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float)
|
||||
cg.add(trigger.set_max(template_))
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
|
||||
|
||||
async def setup_number_core_(
|
||||
var, config, *, min_value: float, max_value: float, step: float
|
||||
):
|
||||
@@ -254,19 +271,7 @@ async def setup_number_core_(
|
||||
if config[CONF_MODE] != NumberMode.NUMBER_MODE_AUTO:
|
||||
cg.add(var.traits.set_mode(config[CONF_MODE]))
|
||||
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
for conf in config.get(CONF_ON_VALUE_RANGE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await cg.register_component(trigger, conf)
|
||||
if CONF_ABOVE in conf:
|
||||
template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float)
|
||||
cg.add(trigger.set_min(template_))
|
||||
if CONF_BELOW in conf:
|
||||
template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float)
|
||||
cg.add(trigger.set_max(template_))
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
CORE.add_job(_build_number_automations, var, config)
|
||||
|
||||
if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None:
|
||||
cg.add(var.traits.set_unit_of_measurement(unit_of_measurement))
|
||||
|
||||
@@ -50,8 +50,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput),
|
||||
cv.Optional(CONF_DEADBAND_PARAMETERS): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_THRESHOLD_HIGH): cv.temperature,
|
||||
cv.Required(CONF_THRESHOLD_LOW): cv.temperature,
|
||||
cv.Required(CONF_THRESHOLD_HIGH): cv.temperature_delta,
|
||||
cv.Required(CONF_THRESHOLD_LOW): cv.temperature_delta,
|
||||
cv.Optional(CONF_KP_MULTIPLIER, default=0.1): cv.float_,
|
||||
cv.Optional(CONF_KI_MULTIPLIER, default=0.0): cv.float_,
|
||||
cv.Optional(CONF_KD_MULTIPLIER, default=0.0): cv.float_,
|
||||
|
||||
@@ -139,9 +139,10 @@ void Rtttl::loop() {
|
||||
x++;
|
||||
}
|
||||
if (x > 0) {
|
||||
int send = this->speaker_->play((uint8_t *) (&sample), x * 2);
|
||||
if (send != x * 4) {
|
||||
this->samples_sent_ -= (x - (send / 2));
|
||||
size_t bytes_to_send = x * sizeof(SpeakerSample);
|
||||
size_t send = this->speaker_->play((uint8_t *) (&sample), bytes_to_send);
|
||||
if (send != bytes_to_send) {
|
||||
this->samples_sent_ -= (x - (send / sizeof(SpeakerSample)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -201,9 +202,9 @@ void Rtttl::loop() {
|
||||
bool need_note_gap = false;
|
||||
if (note) {
|
||||
auto note_index = (scale - 4) * 12 + note;
|
||||
if (note_index < 0 || note_index >= (int) sizeof(NOTES)) {
|
||||
if (note_index < 0 || note_index >= (int) (sizeof(NOTES) / sizeof(NOTES[0]))) {
|
||||
ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index,
|
||||
(int) sizeof(NOTES));
|
||||
(int) (sizeof(NOTES) / sizeof(NOTES[0])));
|
||||
this->finish_();
|
||||
return;
|
||||
}
|
||||
@@ -221,7 +222,7 @@ void Rtttl::loop() {
|
||||
|
||||
#ifdef USE_OUTPUT
|
||||
if (this->output_ != nullptr) {
|
||||
if (need_note_gap) {
|
||||
if (need_note_gap && this->note_duration_ > DOUBLE_NOTE_GAP_MS) {
|
||||
this->output_->set_level(0.0);
|
||||
delay(DOUBLE_NOTE_GAP_MS);
|
||||
this->note_duration_ -= DOUBLE_NOTE_GAP_MS;
|
||||
@@ -240,9 +241,9 @@ void Rtttl::loop() {
|
||||
this->samples_sent_ = 0;
|
||||
this->samples_gap_ = 0;
|
||||
this->samples_per_wave_ = 0;
|
||||
this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1600; //(ms);
|
||||
this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1000;
|
||||
if (need_note_gap) {
|
||||
this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1600; //(ms);
|
||||
this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1000;
|
||||
}
|
||||
if (this->output_freq_ != 0) {
|
||||
// make sure there is enough samples to add a full last sinus.
|
||||
@@ -279,7 +280,7 @@ void Rtttl::play(std::string rtttl) {
|
||||
this->note_duration_ = 0;
|
||||
|
||||
int bpm = 63;
|
||||
uint8_t num;
|
||||
uint16_t num;
|
||||
|
||||
// Get name
|
||||
this->position_ = this->rtttl_.find(':');
|
||||
@@ -395,7 +396,7 @@ void Rtttl::finish_() {
|
||||
sample[0].right = 0;
|
||||
sample[1].left = 0;
|
||||
sample[1].right = 0;
|
||||
this->speaker_->play((uint8_t *) (&sample), 8);
|
||||
this->speaker_->play((uint8_t *) (&sample), sizeof(sample));
|
||||
this->speaker_->finish();
|
||||
this->set_state_(State::STOPPING);
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@ class Rtttl : public Component {
|
||||
}
|
||||
|
||||
protected:
|
||||
inline uint8_t get_integer_() {
|
||||
uint8_t ret = 0;
|
||||
inline uint16_t get_integer_() {
|
||||
uint16_t ret = 0;
|
||||
while (isdigit(this->rtttl_[this->position_])) {
|
||||
ret = (ret * 10) + (this->rtttl_[this->position_++] - '0');
|
||||
}
|
||||
@@ -87,7 +87,7 @@ class Rtttl : public Component {
|
||||
|
||||
#ifdef USE_OUTPUT
|
||||
/// The output to write the sound to.
|
||||
output::FloatOutput *output_;
|
||||
output::FloatOutput *output_{nullptr};
|
||||
#endif // USE_OUTPUT
|
||||
|
||||
#ifdef USE_SPEAKER
|
||||
|
||||
@@ -603,7 +603,7 @@ DELTA_SCHEMA = cv.Any(
|
||||
def _get_delta(value):
|
||||
if isinstance(value, str):
|
||||
assert value.endswith("%")
|
||||
return 0.0, float(value[:-1])
|
||||
return 0.0, float(value[:-1]) / 100.0
|
||||
return value, 0.0
|
||||
|
||||
|
||||
@@ -888,6 +888,26 @@ async def build_filters(config):
|
||||
return await cg.build_registry_list(FILTER_REGISTRY, config)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_sensor_automations(var, config):
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
for conf in config.get(CONF_ON_RAW_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
for conf in config.get(CONF_ON_VALUE_RANGE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await cg.register_component(trigger, conf)
|
||||
if (above := conf.get(CONF_ABOVE)) is not None:
|
||||
template_ = await cg.templatable(above, [(float, "x")], float)
|
||||
cg.add(trigger.set_min(template_))
|
||||
if (below := conf.get(CONF_BELOW)) is not None:
|
||||
template_ = await cg.templatable(below, [(float, "x")], float)
|
||||
cg.add(trigger.set_max(template_))
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
|
||||
|
||||
async def setup_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "sensor")
|
||||
|
||||
@@ -906,22 +926,7 @@ async def setup_sensor_core_(var, config):
|
||||
filters = await build_filters(config[CONF_FILTERS])
|
||||
cg.add(var.set_filters(filters))
|
||||
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
for conf in config.get(CONF_ON_RAW_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
for conf in config.get(CONF_ON_VALUE_RANGE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await cg.register_component(trigger, conf)
|
||||
if (above := conf.get(CONF_ABOVE)) is not None:
|
||||
template_ = await cg.templatable(above, [(float, "x")], float)
|
||||
cg.add(trigger.set_min(template_))
|
||||
if (below := conf.get(CONF_BELOW)) is not None:
|
||||
template_ = await cg.templatable(below, [(float, "x")], float)
|
||||
cg.add(trigger.set_max(template_))
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
CORE.add_job(_build_sensor_automations, var, config)
|
||||
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
|
||||
@@ -84,32 +84,30 @@ SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler
|
||||
: controller_(controller), valve_(valve) {}
|
||||
|
||||
void SprinklerValveOperator::loop() {
|
||||
// Use wrapping subtraction so 32-bit millis() rollover is handled correctly:
|
||||
// (now - start) yields the true elapsed time even across the 49.7-day boundary.
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
if (now >= this->start_millis_) { // dummy check
|
||||
switch (this->state_) {
|
||||
case STARTING:
|
||||
if (now > (this->start_millis_ + this->start_delay_)) {
|
||||
this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state
|
||||
}
|
||||
break;
|
||||
switch (this->state_) {
|
||||
case STARTING:
|
||||
if ((now - *this->start_millis_) > this->start_delay_) {
|
||||
this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state
|
||||
}
|
||||
break;
|
||||
|
||||
case ACTIVE:
|
||||
if (now > (this->start_millis_ + this->start_delay_ + this->run_duration_)) {
|
||||
this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down
|
||||
}
|
||||
break;
|
||||
case ACTIVE:
|
||||
if ((now - *this->start_millis_) > (this->start_delay_ + this->run_duration_)) {
|
||||
this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down
|
||||
}
|
||||
break;
|
||||
|
||||
case STOPPING:
|
||||
if (now > (this->stop_millis_ + this->stop_delay_)) {
|
||||
this->kill_(); // stop_delay_has been exceeded, ensure all valves are off
|
||||
}
|
||||
break;
|
||||
case STOPPING:
|
||||
if ((now - *this->stop_millis_) > this->stop_delay_) {
|
||||
this->kill_(); // stop_delay_has been exceeded, ensure all valves are off
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else { // perhaps millis() rolled over...or something else is horribly wrong!
|
||||
this->stop(); // bail out (TODO: handle this highly unlikely situation better...)
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,11 +122,11 @@ void SprinklerValveOperator::set_valve(SprinklerValve *valve) {
|
||||
if (this->state_ != IDLE) { // Only kill if not already idle
|
||||
this->kill_(); // ensure everything is off before we let go!
|
||||
}
|
||||
this->state_ = IDLE; // reset state
|
||||
this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it
|
||||
this->start_millis_ = 0; // reset because (new) valve has not been started yet
|
||||
this->stop_millis_ = 0; // reset because (new) valve has not been started yet
|
||||
this->valve_ = valve; // finally, set the pointer to the new valve
|
||||
this->state_ = IDLE; // reset state
|
||||
this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it
|
||||
this->start_millis_.reset(); // reset because (new) valve has not been started yet
|
||||
this->stop_millis_.reset(); // reset because (new) valve has not been started yet
|
||||
this->valve_ = valve; // finally, set the pointer to the new valve
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +160,7 @@ void SprinklerValveOperator::start() {
|
||||
} else {
|
||||
this->run_(); // there is no start_delay_, so just start the pump and valve
|
||||
}
|
||||
this->stop_millis_ = 0;
|
||||
this->stop_millis_.reset();
|
||||
this->start_millis_ = millis(); // save the time the start request was made
|
||||
}
|
||||
|
||||
@@ -189,22 +187,25 @@ void SprinklerValveOperator::stop() {
|
||||
uint32_t SprinklerValveOperator::run_duration() { return this->run_duration_ / 1000; }
|
||||
|
||||
uint32_t SprinklerValveOperator::time_remaining() {
|
||||
if (this->start_millis_ == 0) {
|
||||
if (!this->start_millis_.has_value()) {
|
||||
return this->run_duration(); // hasn't been started yet
|
||||
}
|
||||
|
||||
if (this->stop_millis_) {
|
||||
if (this->stop_millis_ - this->start_millis_ >= this->start_delay_ + this->run_duration_) {
|
||||
if (this->stop_millis_.has_value()) {
|
||||
uint32_t elapsed = *this->stop_millis_ - *this->start_millis_;
|
||||
if (elapsed >= this->start_delay_ + this->run_duration_) {
|
||||
return 0; // valve was active for more than its configured duration, so we are done
|
||||
} else {
|
||||
// we're stopped; return time remaining
|
||||
return (this->run_duration_ - (this->stop_millis_ - this->start_millis_)) / 1000;
|
||||
}
|
||||
if (elapsed <= this->start_delay_) {
|
||||
return this->run_duration_ / 1000; // stopped during start delay, full run duration remains
|
||||
}
|
||||
return (this->run_duration_ - (elapsed - this->start_delay_)) / 1000;
|
||||
}
|
||||
|
||||
auto completed_millis = this->start_millis_ + this->start_delay_ + this->run_duration_;
|
||||
if (completed_millis > millis()) {
|
||||
return (completed_millis - millis()) / 1000; // running now
|
||||
uint32_t elapsed = millis() - *this->start_millis_;
|
||||
uint32_t total_duration = this->start_delay_ + this->run_duration_;
|
||||
if (elapsed < total_duration) {
|
||||
return (total_duration - elapsed) / 1000; // running now
|
||||
}
|
||||
return 0; // run completed
|
||||
}
|
||||
@@ -593,7 +594,7 @@ void Sprinkler::set_repeat(optional<uint32_t> repeat) {
|
||||
if (this->repeat_number_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (this->repeat_number_->state == repeat.value()) {
|
||||
if (this->repeat_number_->state == repeat.value_or(0)) {
|
||||
return;
|
||||
}
|
||||
auto call = this->repeat_number_->make_call();
|
||||
@@ -793,7 +794,7 @@ void Sprinkler::start_single_valve(const optional<size_t> valve_number, optional
|
||||
void Sprinkler::queue_valve(optional<size_t> valve_number, optional<uint32_t> run_duration) {
|
||||
if (valve_number.has_value()) {
|
||||
if (this->is_a_valid_valve(valve_number.value()) && (this->queued_valves_.size() < this->max_queue_size_)) {
|
||||
SprinklerQueueItem item{valve_number.value(), run_duration.value()};
|
||||
SprinklerQueueItem item{valve_number.value(), run_duration.value_or(0)};
|
||||
this->queued_valves_.insert(this->queued_valves_.begin(), item);
|
||||
ESP_LOGD(TAG, "Valve %zu placed into queue with run duration of %" PRIu32 " seconds", valve_number.value_or(0),
|
||||
run_duration.value_or(0));
|
||||
@@ -1080,7 +1081,7 @@ uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() {
|
||||
}
|
||||
}
|
||||
|
||||
if (incomplete_valve_count >= enabled_valve_count) {
|
||||
if (incomplete_valve_count > 0 && incomplete_valve_count >= enabled_valve_count) {
|
||||
incomplete_valve_count--;
|
||||
}
|
||||
if (incomplete_valve_count) {
|
||||
|
||||
@@ -141,8 +141,8 @@ class SprinklerValveOperator {
|
||||
uint32_t start_delay_{0};
|
||||
uint32_t stop_delay_{0};
|
||||
uint32_t run_duration_{0};
|
||||
uint64_t start_millis_{0};
|
||||
uint64_t stop_millis_{0};
|
||||
optional<uint32_t> start_millis_{};
|
||||
optional<uint32_t> stop_millis_{};
|
||||
Sprinkler *controller_{nullptr};
|
||||
SprinklerValve *valve_{nullptr};
|
||||
SprinklerState state_{IDLE};
|
||||
|
||||
@@ -141,11 +141,8 @@ def switch_schema(
|
||||
return _SWITCH_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
async def setup_switch_core_(var, config):
|
||||
await setup_entity(var, config, "switch")
|
||||
|
||||
if (inverted := config.get(CONF_INVERTED)) is not None:
|
||||
cg.add(var.set_inverted(inverted))
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_switch_automations(var, config):
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(bool, "x")], conf)
|
||||
@@ -156,6 +153,15 @@ async def setup_switch_core_(var, config):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
|
||||
async def setup_switch_core_(var, config):
|
||||
await setup_entity(var, config, "switch")
|
||||
|
||||
if (inverted := config.get(CONF_INVERTED)) is not None:
|
||||
cg.add(var.set_inverted(inverted))
|
||||
|
||||
CORE.add_job(_build_switch_automations, var, config)
|
||||
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
|
||||
@@ -197,6 +197,17 @@ async def build_filters(config):
|
||||
return await cg.build_registry_list(FILTER_REGISTRY, config)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_text_sensor_automations(var, config):
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_RAW_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
|
||||
|
||||
|
||||
async def setup_text_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "text_sensor")
|
||||
|
||||
@@ -207,13 +218,7 @@ async def setup_text_sensor_core_(var, config):
|
||||
filters = await build_filters(config[CONF_FILTERS])
|
||||
cg.add(var.set_filters(filters))
|
||||
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_RAW_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
|
||||
CORE.add_job(_build_text_sensor_automations, var, config)
|
||||
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "soc/gpio_num.h"
|
||||
#include "soc/uart_pins.h"
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
@@ -19,13 +18,6 @@ namespace esphome::uart {
|
||||
|
||||
static const char *const TAG = "uart.idf";
|
||||
|
||||
/// Check if a pin number matches one of the default UART0 GPIO pins.
|
||||
/// These pins may have residual state from the boot console that requires
|
||||
/// explicit reset before UART reconfiguration (ESP-IDF issue #17459).
|
||||
static constexpr bool is_default_uart0_pin(int8_t pin_num) {
|
||||
return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM;
|
||||
}
|
||||
|
||||
uart_config_t IDFUARTComponent::get_config_() {
|
||||
uart_parity_t parity = UART_PARITY_DISABLE;
|
||||
if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
|
||||
@@ -149,34 +141,12 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
return;
|
||||
}
|
||||
|
||||
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
||||
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
|
||||
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
|
||||
|
||||
// Workaround for ESP-IDF issue: https://github.com/espressif/esp-idf/issues/17459
|
||||
// Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks
|
||||
// UART on default UART0 pins that may have residual state from boot console.
|
||||
// Reset these pins before configuring UART to ensure they're in a clean state.
|
||||
if (is_default_uart0_pin(tx)) {
|
||||
gpio_reset_pin(static_cast<gpio_num_t>(tx));
|
||||
}
|
||||
if (is_default_uart0_pin(rx)) {
|
||||
gpio_reset_pin(static_cast<gpio_num_t>(rx));
|
||||
}
|
||||
|
||||
// Setup pins after reset to configure GPIO direction and pull resistors.
|
||||
// For UART0 default pins, setup() must always be called because gpio_reset_pin()
|
||||
// above sets GPIO_MODE_DISABLE which disables the input buffer. Without setup(),
|
||||
// uart_set_pin() on ESP-IDF 5.4.2+ does not re-enable the input buffer for
|
||||
// IOMUX-connected pins, so the RX pin cannot receive data (see issue #10132).
|
||||
// For other pins, only call setup() if pull or open-drain flags are set to avoid
|
||||
// disturbing the default pin state which breaks some external components (#11823).
|
||||
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
|
||||
if (!pin) {
|
||||
return;
|
||||
}
|
||||
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
|
||||
if (is_default_uart0_pin(pin->get_pin()) || (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
|
||||
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
|
||||
pin->setup();
|
||||
}
|
||||
};
|
||||
@@ -186,6 +156,10 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
setup_pin_if_needed(this->tx_pin_);
|
||||
}
|
||||
|
||||
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
||||
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
|
||||
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
|
||||
|
||||
uint32_t invert = 0;
|
||||
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) {
|
||||
invert |= UART_SIGNAL_TXD_INV;
|
||||
@@ -193,6 +167,9 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) {
|
||||
invert |= UART_SIGNAL_RXD_INV;
|
||||
}
|
||||
if (this->flow_control_pin_ != nullptr && this->flow_control_pin_->is_inverted()) {
|
||||
invert |= UART_SIGNAL_RTS_INV;
|
||||
}
|
||||
|
||||
err = uart_set_line_inverse(this->uart_num_, invert);
|
||||
if (err != ESP_OK) {
|
||||
|
||||
@@ -2048,7 +2048,7 @@ bool WiFiComponent::can_proceed() {
|
||||
#endif
|
||||
|
||||
void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
bool WiFiComponent::is_connected() {
|
||||
bool WiFiComponent::is_connected() const {
|
||||
return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED &&
|
||||
this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_;
|
||||
}
|
||||
|
||||
@@ -430,7 +430,7 @@ class WiFiComponent : public Component {
|
||||
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
|
||||
bool is_connected();
|
||||
bool is_connected() const;
|
||||
|
||||
void set_power_save_mode(WiFiPowerSaveMode power_save);
|
||||
void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; }
|
||||
@@ -665,7 +665,7 @@ class WiFiComponent : public Component {
|
||||
bool wifi_apply_hostname_();
|
||||
bool wifi_sta_connect_(const WiFiAP &ap);
|
||||
void wifi_pre_setup_();
|
||||
WiFiSTAConnectStatus wifi_sta_connect_status_();
|
||||
WiFiSTAConnectStatus wifi_sta_connect_status_() const;
|
||||
bool wifi_scan_start_(bool passive);
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
|
||||
@@ -626,7 +626,7 @@ void WiFiComponent::wifi_pre_setup_() {
|
||||
this->wifi_mode_(false, false);
|
||||
}
|
||||
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
|
||||
station_status_t status = wifi_station_get_connect_status();
|
||||
if (status == STATION_GOT_IP)
|
||||
return WiFiSTAConnectStatus::CONNECTED;
|
||||
|
||||
@@ -914,7 +914,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
}
|
||||
}
|
||||
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
|
||||
if (s_sta_connected && this->got_ipv4_address_) {
|
||||
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
|
||||
if (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT) {
|
||||
|
||||
@@ -621,7 +621,7 @@ void WiFiComponent::wifi_pre_setup_() {
|
||||
// Make sure WiFi is in clean state before anything starts
|
||||
this->wifi_mode_(false, false);
|
||||
}
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
|
||||
// Use state machine instead of querying WiFi.status() directly
|
||||
// State is updated in main loop from queued events, ensuring thread safety
|
||||
switch (s_sta_state) {
|
||||
|
||||
@@ -115,7 +115,7 @@ const char *get_disconnect_reason_str(uint8_t reason) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
|
||||
int status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA);
|
||||
switch (status) {
|
||||
case CYW43_LINK_JOIN:
|
||||
|
||||
@@ -8,7 +8,7 @@ from esphome.components.zephyr import zephyr_add_pm_static, zephyr_data
|
||||
from esphome.components.zephyr.const import KEY_BOOTLOADER
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERNAL, CONF_NAME
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .const_zephyr import (
|
||||
@@ -96,6 +96,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_ZIGBEE")
|
||||
if CORE.using_zephyr:
|
||||
|
||||
@@ -179,6 +179,13 @@ async def zephyr_to_code(config: ConfigType) -> None:
|
||||
"USE_ZIGBEE_WIPE_ON_BOOT_MAGIC", random.randint(0x000001, 0xFFFFFF)
|
||||
)
|
||||
cg.add_define("USE_ZIGBEE_WIPE_ON_BOOT")
|
||||
|
||||
# Generate attribute lists before any await that could yield (e.g., build_automation
|
||||
# waiting for variables from other components). If the hub's priority decays while
|
||||
# yielding, deferred entity jobs may add cluster list globals that reference these
|
||||
# attribute lists before they're declared.
|
||||
await _attr_to_code(config)
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
if on_join_config := config.get(CONF_ON_JOIN):
|
||||
@@ -186,7 +193,6 @@ async def zephyr_to_code(config: ConfigType) -> None:
|
||||
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await _attr_to_code(config)
|
||||
CORE.add_job(_ctx_to_code, config)
|
||||
|
||||
|
||||
|
||||
+1
-2
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.2.1"
|
||||
__version__ = "2026.2.4"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
@@ -943,7 +943,6 @@ CONF_SPI = "spi"
|
||||
CONF_SPI_ID = "spi_id"
|
||||
CONF_SPIKE_REJECTION = "spike_rejection"
|
||||
CONF_SSID = "ssid"
|
||||
CONF_SSL_FINGERPRINTS = "ssl_fingerprints"
|
||||
CONF_STARTUP_DELAY = "startup_delay"
|
||||
CONF_STATE = "state"
|
||||
CONF_STATE_CLASS = "state_class"
|
||||
|
||||
@@ -132,7 +132,7 @@ void Application::setup() {
|
||||
this->after_loop_tasks_();
|
||||
this->app_state_ = new_app_state;
|
||||
yield();
|
||||
} while (!component->can_proceed());
|
||||
} while (!component->can_proceed() && !component->is_failed());
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "setup() finished successfully!");
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
#define USE_HOMEASSISTANT_TIME
|
||||
#define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT
|
||||
#define USE_IMAGE
|
||||
#define USE_IMPROV_SERIAL
|
||||
#define USE_IMPROV_SERIAL_NEXT_URL
|
||||
#define USE_INFRARED
|
||||
#define USE_IR_RF
|
||||
|
||||
+2
-27
@@ -1,6 +1,5 @@
|
||||
import contextlib
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import ssl
|
||||
@@ -22,14 +21,12 @@ from esphome.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_SKIP_CERT_CN_CHECK,
|
||||
CONF_SSL_FINGERPRINTS,
|
||||
CONF_TOPIC,
|
||||
CONF_TOPIC_PREFIX,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.helpers import get_int_env, get_str_env
|
||||
from esphome.log import AnsiFore, color
|
||||
from esphome.types import ConfigType
|
||||
from esphome.util import safe_print
|
||||
|
||||
@@ -102,9 +99,7 @@ def prepare(
|
||||
elif username:
|
||||
client.username_pw_set(username, password)
|
||||
|
||||
if config[CONF_MQTT].get(CONF_SSL_FINGERPRINTS) or config[CONF_MQTT].get(
|
||||
CONF_CERTIFICATE_AUTHORITY
|
||||
):
|
||||
if config[CONF_MQTT].get(CONF_CERTIFICATE_AUTHORITY):
|
||||
context = ssl.create_default_context(
|
||||
cadata=config[CONF_MQTT].get(CONF_CERTIFICATE_AUTHORITY)
|
||||
)
|
||||
@@ -283,23 +278,3 @@ def clear_topic(config, topic, username=None, password=None, client_id=None):
|
||||
client.publish(msg.topic, None, retain=True)
|
||||
|
||||
return initialize(config, [topic], on_message, None, username, password, client_id)
|
||||
|
||||
|
||||
# From marvinroger/async-mqtt-client -> scripts/get-fingerprint/get-fingerprint.py
|
||||
def get_fingerprint(config):
|
||||
addr = str(config[CONF_MQTT][CONF_BROKER]), int(config[CONF_MQTT][CONF_PORT])
|
||||
_LOGGER.info("Getting fingerprint from %s:%s", addr[0], addr[1])
|
||||
try:
|
||||
cert_pem = ssl.get_server_certificate(addr)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Unable to connect to server: %s", err)
|
||||
return 1
|
||||
cert_der = ssl.PEM_cert_to_DER_cert(cert_pem)
|
||||
|
||||
sha1 = hashlib.sha1(cert_der).hexdigest()
|
||||
|
||||
safe_print(f"SHA1 Fingerprint: {color(AnsiFore.CYAN, sha1)}")
|
||||
safe_print(
|
||||
f"Copy the string above into mqtt.ssl_fingerprints section of {CORE.config_path}"
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -15,8 +15,30 @@ esp_ldo:
|
||||
|
||||
display:
|
||||
- platform: mipi_dsi
|
||||
id: p4_nano
|
||||
model: WAVESHARE-P4-NANO-10.1
|
||||
|
||||
rotation: 90
|
||||
- platform: mipi_dsi
|
||||
id: p4_86
|
||||
model: "WAVESHARE-P4-86-PANEL"
|
||||
rotation: 180
|
||||
- platform: mipi_dsi
|
||||
model: custom
|
||||
id: custom_id
|
||||
dimensions:
|
||||
width: 400
|
||||
height: 1280
|
||||
hsync_back_porch: 40
|
||||
hsync_pulse_width: 30
|
||||
hsync_front_porch: 40
|
||||
vsync_back_porch: 20
|
||||
vsync_pulse_width: 10
|
||||
vsync_front_porch: 20
|
||||
pclk_frequency: 48Mhz
|
||||
lane_bit_rate: 1.2Gbps
|
||||
rotation: 180
|
||||
transform: disabled
|
||||
init_sequence:
|
||||
i2c:
|
||||
sda: GPIO7
|
||||
scl: GPIO8
|
||||
|
||||
@@ -119,9 +119,12 @@ def test_code_generation(
|
||||
|
||||
main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml"))
|
||||
assert (
|
||||
"mipi_dsi_mipi_dsi_id = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);"
|
||||
"p4_nano = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);"
|
||||
in main_cpp
|
||||
)
|
||||
assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp
|
||||
assert "mipi_dsi_mipi_dsi_id->set_lane_bit_rate(1500);" in main_cpp
|
||||
assert "p4_nano->set_lane_bit_rate(1500.0f);" in main_cpp
|
||||
assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp
|
||||
assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp
|
||||
assert "custom_id->set_rotation(display::DISPLAY_ROTATION_180_DEGREES);" in main_cpp
|
||||
# assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,4 +1,13 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- lambda: |-
|
||||
// Test deprecated std::string overload still compiles
|
||||
std::string key = "00112233445566778899aabbccddeeff";
|
||||
id(dsmr_instance).set_decryption_key(key);
|
||||
|
||||
dsmr:
|
||||
id: dsmr_instance
|
||||
decryption_key: 00112233445566778899aabbccddeeff
|
||||
max_telegram_length: 1000
|
||||
request_pin: ${request_pin}
|
||||
|
||||
@@ -90,6 +90,19 @@ text_sensor:
|
||||
id: ha_hello_world_text2
|
||||
attribute: some_attribute
|
||||
|
||||
event:
|
||||
- platform: template
|
||||
name: Test Event
|
||||
id: test_event
|
||||
event_types:
|
||||
- test_event_type
|
||||
on_event:
|
||||
- homeassistant.event:
|
||||
event: esphome.test_event
|
||||
data:
|
||||
event_name: !lambda |-
|
||||
return event_type;
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
on_time:
|
||||
|
||||
@@ -13,10 +13,10 @@ esphome:
|
||||
on_boot:
|
||||
- priority: 100
|
||||
then:
|
||||
- max7129digit.invert_off:
|
||||
- max7129digit.invert_on:
|
||||
- max7129digit.turn_on:
|
||||
- max7129digit.turn_off:
|
||||
- max7129digit.reverse_on:
|
||||
- max7129digit.reverse_off:
|
||||
- max7129digit.intensity: 10
|
||||
- max7219digit.invert_off:
|
||||
- max7219digit.invert_on:
|
||||
- max7219digit.turn_on:
|
||||
- max7219digit.turn_off:
|
||||
- max7219digit.reverse_on:
|
||||
- max7219digit.reverse_off:
|
||||
- max7219digit.intensity: 10
|
||||
|
||||
@@ -46,6 +46,7 @@ sensor:
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: motion_detected
|
||||
name: Motion Detected
|
||||
device_id: motion_sensor
|
||||
lambda: return true;
|
||||
@@ -82,3 +83,117 @@ output:
|
||||
write_action:
|
||||
- lambda: |-
|
||||
ESP_LOGD("test", "Light output: %d", state);
|
||||
|
||||
cover:
|
||||
- platform: template
|
||||
name: Garage Door
|
||||
device_id: motion_sensor
|
||||
optimistic: true
|
||||
|
||||
fan:
|
||||
- platform: template
|
||||
name: Ceiling Fan
|
||||
device_id: humidity_monitor
|
||||
speed_count: 3
|
||||
has_oscillating: false
|
||||
has_direction: false
|
||||
|
||||
lock:
|
||||
- platform: template
|
||||
name: Front Door Lock
|
||||
device_id: motion_sensor
|
||||
optimistic: true
|
||||
|
||||
number:
|
||||
- platform: template
|
||||
name: Target Temperature
|
||||
device_id: temperature_monitor
|
||||
optimistic: true
|
||||
min_value: 0
|
||||
max_value: 100
|
||||
step: 1
|
||||
|
||||
select:
|
||||
- platform: template
|
||||
name: Mode Select
|
||||
device_id: humidity_monitor
|
||||
optimistic: true
|
||||
options:
|
||||
- "Auto"
|
||||
- "Manual"
|
||||
|
||||
text:
|
||||
- platform: template
|
||||
name: Device Label
|
||||
device_id: temperature_monitor
|
||||
optimistic: true
|
||||
mode: text
|
||||
|
||||
valve:
|
||||
- platform: template
|
||||
name: Water Valve
|
||||
device_id: humidity_monitor
|
||||
optimistic: true
|
||||
|
||||
globals:
|
||||
- id: global_away
|
||||
type: bool
|
||||
initial_value: "false"
|
||||
- id: global_is_on
|
||||
type: bool
|
||||
initial_value: "true"
|
||||
|
||||
water_heater:
|
||||
- platform: template
|
||||
name: Test Boiler
|
||||
device_id: temperature_monitor
|
||||
optimistic: true
|
||||
current_temperature: !lambda "return 45.0f;"
|
||||
target_temperature: !lambda "return 60.0f;"
|
||||
away: !lambda "return id(global_away);"
|
||||
is_on: !lambda "return id(global_is_on);"
|
||||
supported_modes:
|
||||
- "off"
|
||||
- electric
|
||||
visual:
|
||||
min_temperature: 30.0
|
||||
max_temperature: 85.0
|
||||
target_temperature_step: 0.5
|
||||
set_action:
|
||||
- lambda: |-
|
||||
ESP_LOGD("test", "Water heater set");
|
||||
|
||||
alarm_control_panel:
|
||||
- platform: template
|
||||
name: House Alarm
|
||||
device_id: motion_sensor
|
||||
codes:
|
||||
- "1234"
|
||||
restore_mode: ALWAYS_DISARMED
|
||||
binary_sensors:
|
||||
- input: motion_detected
|
||||
|
||||
datetime:
|
||||
- platform: template
|
||||
name: Schedule Date
|
||||
device_id: temperature_monitor
|
||||
type: date
|
||||
optimistic: true
|
||||
- platform: template
|
||||
name: Schedule Time
|
||||
device_id: humidity_monitor
|
||||
type: time
|
||||
optimistic: true
|
||||
- platform: template
|
||||
name: Schedule DateTime
|
||||
device_id: motion_sensor
|
||||
type: datetime
|
||||
optimistic: true
|
||||
|
||||
event:
|
||||
- platform: template
|
||||
name: Doorbell
|
||||
device_id: motion_sensor
|
||||
event_types:
|
||||
- "press"
|
||||
- "double_press"
|
||||
|
||||
@@ -28,6 +28,11 @@ sensor:
|
||||
id: source_sensor_4
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Sensor 5"
|
||||
id: source_sensor_5
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor_1
|
||||
name: "Filter Min"
|
||||
@@ -69,6 +74,13 @@ sensor:
|
||||
filters:
|
||||
- delta: 0
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor_5
|
||||
name: "Filter Percentage"
|
||||
id: filter_percentage
|
||||
filters:
|
||||
- delta: 50%
|
||||
|
||||
script:
|
||||
- id: test_filter_min
|
||||
then:
|
||||
@@ -154,6 +166,28 @@ script:
|
||||
id: source_sensor_4
|
||||
state: 2.0
|
||||
|
||||
- id: test_filter_percentage
|
||||
then:
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: 100.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: 120.0 # Filtered out (delta=20, need >50)
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: 160.0 # Passes (delta=60 > 50% of 100=50)
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: 200.0 # Filtered out (delta=40, need >50% of 160=80)
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: 250.0 # Passes (delta=90 > 80)
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test Filter Min"
|
||||
@@ -178,3 +212,9 @@ button:
|
||||
id: btn_filter_zero_delta
|
||||
on_press:
|
||||
- script.execute: test_filter_zero_delta
|
||||
|
||||
- platform: template
|
||||
name: "Test Filter Percentage"
|
||||
id: btn_filter_percentage
|
||||
on_press:
|
||||
- script.execute: test_filter_percentage
|
||||
|
||||
@@ -4,11 +4,80 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import BinarySensorState, EntityState, SensorState, TextSensorState
|
||||
from aioesphomeapi import (
|
||||
AlarmControlPanelEntityState,
|
||||
BinarySensorState,
|
||||
CoverState,
|
||||
DateState,
|
||||
DateTimeState,
|
||||
EntityState,
|
||||
FanState,
|
||||
LightState,
|
||||
LockEntityState,
|
||||
NumberState,
|
||||
SelectState,
|
||||
SensorState,
|
||||
SwitchState,
|
||||
TextSensorState,
|
||||
TextState,
|
||||
TimeState,
|
||||
ValveState,
|
||||
WaterHeaterState,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# Mapping of entity name to device name for all entities with device_id
|
||||
ENTITY_TO_DEVICE = {
|
||||
# Original entities
|
||||
"Temperature": "Temperature Monitor",
|
||||
"Humidity": "Humidity Monitor",
|
||||
"Motion Detected": "Motion Sensor",
|
||||
"Temperature Monitor Power": "Temperature Monitor",
|
||||
"Temperature Status": "Temperature Monitor",
|
||||
"Motion Light": "Motion Sensor",
|
||||
# New entity types
|
||||
"Garage Door": "Motion Sensor",
|
||||
"Ceiling Fan": "Humidity Monitor",
|
||||
"Front Door Lock": "Motion Sensor",
|
||||
"Target Temperature": "Temperature Monitor",
|
||||
"Mode Select": "Humidity Monitor",
|
||||
"Device Label": "Temperature Monitor",
|
||||
"Water Valve": "Humidity Monitor",
|
||||
"Test Boiler": "Temperature Monitor",
|
||||
"House Alarm": "Motion Sensor",
|
||||
"Schedule Date": "Temperature Monitor",
|
||||
"Schedule Time": "Humidity Monitor",
|
||||
"Schedule DateTime": "Motion Sensor",
|
||||
"Doorbell": "Motion Sensor",
|
||||
}
|
||||
|
||||
# Entities without device_id (should have device_id 0)
|
||||
NO_DEVICE_ENTITIES = {"No Device Sensor"}
|
||||
|
||||
# State types that should have non-zero device_id, mapped by their aioesphomeapi class
|
||||
EXPECTED_STATE_TYPES = [
|
||||
(SensorState, "sensor"),
|
||||
(BinarySensorState, "binary_sensor"),
|
||||
(SwitchState, "switch"),
|
||||
(TextSensorState, "text_sensor"),
|
||||
(LightState, "light"),
|
||||
(CoverState, "cover"),
|
||||
(FanState, "fan"),
|
||||
(LockEntityState, "lock"),
|
||||
(NumberState, "number"),
|
||||
(SelectState, "select"),
|
||||
(TextState, "text"),
|
||||
(ValveState, "valve"),
|
||||
(WaterHeaterState, "water_heater"),
|
||||
(AlarmControlPanelEntityState, "alarm_control_panel"),
|
||||
(DateState, "date"),
|
||||
(TimeState, "time"),
|
||||
(DateTimeState, "datetime"),
|
||||
# Event is stateless (no initial state sent on subscribe)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_device_id_in_state(
|
||||
@@ -40,34 +109,35 @@ async def test_device_id_in_state(
|
||||
entity_device_mapping: dict[int, int] = {}
|
||||
|
||||
for entity in all_entities:
|
||||
# All entities have name and key attributes
|
||||
if entity.name == "Temperature":
|
||||
entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
|
||||
elif entity.name == "Humidity":
|
||||
entity_device_mapping[entity.key] = device_ids["Humidity Monitor"]
|
||||
elif entity.name == "Motion Detected":
|
||||
entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
|
||||
elif entity.name in {"Temperature Monitor Power", "Temperature Status"}:
|
||||
entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
|
||||
elif entity.name == "Motion Light":
|
||||
entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
|
||||
elif entity.name == "No Device Sensor":
|
||||
# Entity without device_id should have device_id 0
|
||||
if entity.name in ENTITY_TO_DEVICE:
|
||||
expected_device = ENTITY_TO_DEVICE[entity.name]
|
||||
entity_device_mapping[entity.key] = device_ids[expected_device]
|
||||
elif entity.name in NO_DEVICE_ENTITIES:
|
||||
entity_device_mapping[entity.key] = 0
|
||||
|
||||
assert len(entity_device_mapping) >= 6, (
|
||||
f"Expected at least 6 mapped entities, got {len(entity_device_mapping)}"
|
||||
expected_count = len(ENTITY_TO_DEVICE) + len(NO_DEVICE_ENTITIES)
|
||||
assert len(entity_device_mapping) >= expected_count, (
|
||||
f"Expected at least {expected_count} mapped entities, "
|
||||
f"got {len(entity_device_mapping)}. "
|
||||
f"Missing: {set(ENTITY_TO_DEVICE) | NO_DEVICE_ENTITIES - {e.name for e in all_entities}}"
|
||||
)
|
||||
|
||||
# Subscribe to states and wait for all mapped entities
|
||||
# Event entities are stateless (no initial state on subscribe),
|
||||
# so exclude them from the expected count
|
||||
stateless_keys = {e.key for e in all_entities if e.name == "Doorbell"}
|
||||
stateful_count = len(entity_device_mapping) - len(
|
||||
stateless_keys & entity_device_mapping.keys()
|
||||
)
|
||||
|
||||
# Subscribe to states
|
||||
loop = asyncio.get_running_loop()
|
||||
states: dict[int, EntityState] = {}
|
||||
states_future: asyncio.Future[bool] = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
states[state.key] = state
|
||||
# Check if we have states for all mapped entities
|
||||
if len(states) >= len(entity_device_mapping) and not states_future.done():
|
||||
if state.key in entity_device_mapping:
|
||||
states[state.key] = state
|
||||
if len(states) >= stateful_count and not states_future.done():
|
||||
states_future.set_result(True)
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
@@ -76,9 +146,16 @@ async def test_device_id_in_state(
|
||||
try:
|
||||
await asyncio.wait_for(states_future, timeout=10.0)
|
||||
except TimeoutError:
|
||||
received_names = {e.name for e in all_entities if e.key in states}
|
||||
missing_names = (
|
||||
(set(ENTITY_TO_DEVICE) | NO_DEVICE_ENTITIES)
|
||||
- received_names
|
||||
- {"Doorbell"}
|
||||
)
|
||||
pytest.fail(
|
||||
f"Did not receive all entity states within 10 seconds. "
|
||||
f"Received {len(states)} states, expected {len(entity_device_mapping)}"
|
||||
f"Received {len(states)} states. "
|
||||
f"Missing: {missing_names}"
|
||||
)
|
||||
|
||||
# Verify each state has the correct device_id
|
||||
@@ -86,51 +163,33 @@ async def test_device_id_in_state(
|
||||
for key, expected_device_id in entity_device_mapping.items():
|
||||
if key in states:
|
||||
state = states[key]
|
||||
entity_name = next(
|
||||
(e.name for e in all_entities if e.key == key), f"key={key}"
|
||||
)
|
||||
|
||||
assert state.device_id == expected_device_id, (
|
||||
f"State for key {key} has device_id {state.device_id}, "
|
||||
f"expected {expected_device_id}"
|
||||
f"State for '{entity_name}' (type={type(state).__name__}) "
|
||||
f"has device_id {state.device_id}, expected {expected_device_id}"
|
||||
)
|
||||
verified_count += 1
|
||||
|
||||
assert verified_count >= 6, (
|
||||
f"Only verified {verified_count} states, expected at least 6"
|
||||
# All stateful entities should be verified (everything except Doorbell event)
|
||||
expected_verified = expected_count - 1 # exclude Doorbell
|
||||
assert verified_count >= expected_verified, (
|
||||
f"Only verified {verified_count} states, expected at least {expected_verified}"
|
||||
)
|
||||
|
||||
# Test specific state types to ensure device_id is present
|
||||
# Find a sensor state with device_id
|
||||
sensor_state = next(
|
||||
(
|
||||
# Verify each expected state type has at least one instance with non-zero device_id
|
||||
for state_type, type_name in EXPECTED_STATE_TYPES:
|
||||
matching = [
|
||||
s
|
||||
for s in states.values()
|
||||
if isinstance(s, SensorState)
|
||||
and isinstance(s.state, float)
|
||||
and s.device_id != 0
|
||||
),
|
||||
None,
|
||||
)
|
||||
assert sensor_state is not None, "No sensor state with device_id found"
|
||||
assert sensor_state.device_id > 0, "Sensor state should have non-zero device_id"
|
||||
|
||||
# Find a binary sensor state
|
||||
binary_sensor_state = next(
|
||||
(s for s in states.values() if isinstance(s, BinarySensorState)),
|
||||
None,
|
||||
)
|
||||
assert binary_sensor_state is not None, "No binary sensor state found"
|
||||
assert binary_sensor_state.device_id > 0, (
|
||||
"Binary sensor state should have non-zero device_id"
|
||||
)
|
||||
|
||||
# Find a text sensor state
|
||||
text_sensor_state = next(
|
||||
(s for s in states.values() if isinstance(s, TextSensorState)),
|
||||
None,
|
||||
)
|
||||
assert text_sensor_state is not None, "No text sensor state found"
|
||||
assert text_sensor_state.device_id > 0, (
|
||||
"Text sensor state should have non-zero device_id"
|
||||
)
|
||||
if isinstance(s, state_type) and s.device_id != 0
|
||||
]
|
||||
assert matching, (
|
||||
f"No {type_name} state (type={state_type.__name__}) "
|
||||
f"with non-zero device_id found"
|
||||
)
|
||||
|
||||
# Verify the "No Device Sensor" has device_id = 0
|
||||
no_device_key = next(
|
||||
|
||||
@@ -24,12 +24,14 @@ async def test_sensor_filters_delta(
|
||||
"filter_max": [],
|
||||
"filter_baseline_max": [],
|
||||
"filter_zero_delta": [],
|
||||
"filter_percentage": [],
|
||||
}
|
||||
|
||||
filter_min_done = loop.create_future()
|
||||
filter_max_done = loop.create_future()
|
||||
filter_baseline_max_done = loop.create_future()
|
||||
filter_zero_delta_done = loop.create_future()
|
||||
filter_percentage_done = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if not isinstance(state, SensorState) or state.missing_state:
|
||||
@@ -66,6 +68,12 @@ async def test_sensor_filters_delta(
|
||||
and not filter_zero_delta_done.done()
|
||||
):
|
||||
filter_zero_delta_done.set_result(True)
|
||||
elif (
|
||||
sensor_name == "filter_percentage"
|
||||
and len(sensor_values[sensor_name]) == 3
|
||||
and not filter_percentage_done.done()
|
||||
):
|
||||
filter_percentage_done.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
@@ -80,6 +88,7 @@ async def test_sensor_filters_delta(
|
||||
"filter_max": "Filter Max",
|
||||
"filter_baseline_max": "Filter Baseline Max",
|
||||
"filter_zero_delta": "Filter Zero Delta",
|
||||
"filter_percentage": "Filter Percentage",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -98,13 +107,14 @@ async def test_sensor_filters_delta(
|
||||
"Test Filter Max": "filter_max",
|
||||
"Test Filter Baseline Max": "filter_baseline_max",
|
||||
"Test Filter Zero Delta": "filter_zero_delta",
|
||||
"Test Filter Percentage": "filter_percentage",
|
||||
}
|
||||
buttons = {}
|
||||
for entity in entities:
|
||||
if isinstance(entity, ButtonInfo) and entity.name in button_name_map:
|
||||
buttons[button_name_map[entity.name]] = entity.key
|
||||
|
||||
assert len(buttons) == 4, f"Expected 3 buttons, found {len(buttons)}"
|
||||
assert len(buttons) == 5, f"Expected 5 buttons, found {len(buttons)}"
|
||||
|
||||
# Test 1: Min
|
||||
sensor_values["filter_min"].clear()
|
||||
@@ -161,3 +171,18 @@ async def test_sensor_filters_delta(
|
||||
assert sensor_values["filter_zero_delta"] == pytest.approx(expected), (
|
||||
f"Test 4 failed: expected {expected}, got {sensor_values['filter_zero_delta']}"
|
||||
)
|
||||
|
||||
# Test 5: Percentage (delta: 50%)
|
||||
sensor_values["filter_percentage"].clear()
|
||||
client.button_command(buttons["filter_percentage"])
|
||||
try:
|
||||
await asyncio.wait_for(filter_percentage_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Test 5 timed out. Values: {sensor_values['filter_percentage']}"
|
||||
)
|
||||
|
||||
expected = [100.0, 160.0, 250.0]
|
||||
assert sensor_values["filter_percentage"] == pytest.approx(expected), (
|
||||
f"Test 5 failed: expected {expected}, got {sensor_values['filter_percentage']}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user