mirror of
https://github.com/esphome/esphome.git
synced 2026-06-30 04:26:07 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7843582e8 | |||
| 2c749e9dbe | |||
| 8479664df1 | |||
| 5a1d6428b2 | |||
| a39be5a461 | |||
| da930310b1 | |||
| af296eb600 | |||
| 2c11c65faf | |||
| 29d890bb0f | |||
| efa39ae591 | |||
| 4b57ac3236 | |||
| 997f825cd3 | |||
| 27fe866d5e | |||
| c5c6ce6b0e | |||
| 15e2a778d4 | |||
| 1f5a35a99f | |||
| 0975755a9d | |||
| 19f4845185 | |||
| 49356f4132 | |||
| 8aaf0b8d85 | |||
| 28d510191c | |||
| 4c8e0575f9 | |||
| 49afe53a2c | |||
| d19c1b689a | |||
| e7e1acc0a2 | |||
| 7bdeb32a8a | |||
| f412ab4f8b | |||
| 0fc09462ff | |||
| d78496321e | |||
| ac76fc4409 | |||
| a343ff1989 | |||
| 2d2178c90a | |||
| 25b14f9953 | |||
| 2491b4f85c | |||
| cb8b14e64b | |||
| 887172d663 | |||
| e4aa23abaa | |||
| 8c0cc3a2d8 | |||
| efe8a6c8eb |
@@ -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.0
|
||||
PROJECT_NUMBER = 2026.2.2
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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...>() {}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -14,12 +14,17 @@ static const int PORT = 5568;
|
||||
E131Component::E131Component() {}
|
||||
|
||||
E131Component::~E131Component() {
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
if (this->socket_) {
|
||||
this->socket_->close();
|
||||
}
|
||||
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||
this->udp_.stop();
|
||||
#endif
|
||||
}
|
||||
|
||||
void E131Component::setup() {
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP);
|
||||
|
||||
int enable = 1;
|
||||
@@ -50,6 +55,13 @@ void E131Component::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||
if (!this->udp_.begin(PORT)) {
|
||||
ESP_LOGW(TAG, "Cannot bind E1.31 to port %d.", PORT);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
join_igmp_groups_();
|
||||
}
|
||||
@@ -59,19 +71,36 @@ void E131Component::loop() {
|
||||
int universe = 0;
|
||||
uint8_t buf[1460];
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
ssize_t len = this->socket_->read(buf, sizeof(buf));
|
||||
if (len == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->packet_(buf, (size_t) len, universe, packet)) {
|
||||
ESP_LOGV(TAG, "Invalid packet received of size %zd.", len);
|
||||
ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->process_(universe, packet)) {
|
||||
ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
|
||||
}
|
||||
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||
while (auto packet_size = this->udp_.parsePacket()) {
|
||||
auto len = this->udp_.read(buf, sizeof(buf));
|
||||
if (len <= 0)
|
||||
continue;
|
||||
|
||||
if (!this->packet_(buf, (size_t) len, universe, packet)) {
|
||||
ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this->process_(universe, packet)) {
|
||||
ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_NETWORK
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||
#include <WiFiUdp.h>
|
||||
#endif
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include <cinttypes>
|
||||
@@ -45,7 +49,11 @@ class E131Component : public esphome::Component {
|
||||
void leave_(int universe);
|
||||
|
||||
E131ListenMethod listen_method_{E131_MULTICAST};
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
std::unique_ptr<socket::Socket> socket_;
|
||||
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||
WiFiUDP udp_;
|
||||
#endif
|
||||
std::vector<E131AddressableLightEffect *> light_effects_;
|
||||
std::map<int, int> universe_consumers_;
|
||||
};
|
||||
|
||||
@@ -62,8 +62,10 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast<size_t>(&((E131RawPacket *)
|
||||
bool E131Component::join_igmp_groups_() {
|
||||
if (listen_method_ != E131_MULTICAST)
|
||||
return false;
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
if (this->socket_ == nullptr)
|
||||
return false;
|
||||
#endif
|
||||
|
||||
for (auto universe : universe_consumers_) {
|
||||
if (!universe.second)
|
||||
|
||||
@@ -9,6 +9,7 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import socket
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
|
||||
from esphome.components.esp32.const import VARIANT_ESP32C2
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ENABLE_ON_BOOT,
|
||||
@@ -387,6 +388,15 @@ def final_validation(config):
|
||||
f"Name '{name}' is too long, maximum length is {max_length} characters"
|
||||
)
|
||||
|
||||
# ESP32-C2 has very limited RAM (~272KB). Without releasing BLE IRAM,
|
||||
# esp_bt_controller_init fails with ESP_ERR_NO_MEM.
|
||||
# CONFIG_BT_RELEASE_IRAM changes the memory layout so IRAM and DRAM share
|
||||
# space more flexibly, giving the BT controller enough contiguous memory.
|
||||
# This requires CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT to be disabled.
|
||||
if get_esp32_variant() == VARIANT_ESP32C2:
|
||||
add_idf_sdkconfig_option("CONFIG_BT_RELEASE_IRAM", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT", False)
|
||||
|
||||
# Set GATT Client/Server sdkconfig options based on which components are loaded
|
||||
full_config = fv.full_config.get()
|
||||
|
||||
|
||||
@@ -527,7 +527,7 @@ async def to_code_characteristic(service_var, char_conf):
|
||||
action_conf,
|
||||
char_conf[CONF_CHAR_VALUE_ACTION_ID_],
|
||||
cg.TemplateArguments(),
|
||||
{},
|
||||
[],
|
||||
)
|
||||
cg.add(value_action.play())
|
||||
else:
|
||||
|
||||
@@ -246,9 +246,27 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
|
||||
if (this->handle_ != param->write.handle)
|
||||
break;
|
||||
|
||||
esp_gatt_status_t status = ESP_GATT_OK;
|
||||
|
||||
if (param->write.is_prep) {
|
||||
this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len);
|
||||
this->write_event_ = true;
|
||||
const size_t offset = param->write.offset;
|
||||
const size_t write_len = param->write.len;
|
||||
const size_t new_size = offset + write_len;
|
||||
// Clean the buffer on the first prepared write event
|
||||
if (offset == 0) {
|
||||
this->value_.clear();
|
||||
}
|
||||
|
||||
if (offset != this->value_.size()) {
|
||||
status = ESP_GATT_INVALID_OFFSET;
|
||||
} else if (new_size > ESP_GATT_MAX_ATTR_LEN) {
|
||||
status = ESP_GATT_INVALID_ATTR_LEN;
|
||||
} else {
|
||||
if (this->value_.size() < new_size) {
|
||||
this->value_.resize(new_size);
|
||||
}
|
||||
memcpy(this->value_.data() + offset, param->write.value, write_len);
|
||||
}
|
||||
} else {
|
||||
this->set_value(ByteBuffer::wrap(param->write.value, param->write.len));
|
||||
}
|
||||
@@ -263,7 +281,7 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
|
||||
memcpy(response.attr_value.value, param->write.value, param->write.len);
|
||||
|
||||
esp_err_t err =
|
||||
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response);
|
||||
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, &response);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err);
|
||||
@@ -280,9 +298,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
|
||||
}
|
||||
|
||||
case ESP_GATTS_EXEC_WRITE_EVT: {
|
||||
if (!this->write_event_)
|
||||
// BLE stack will guarantee that ESP_GATTS_EXEC_WRITE_EVT is only received after prepared writes
|
||||
if (this->value_.empty())
|
||||
break;
|
||||
this->write_event_ = false;
|
||||
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
|
||||
if (this->on_write_callback_) {
|
||||
(*this->on_write_callback_)(this->value_, param->exec_write.conn_id);
|
||||
|
||||
@@ -77,7 +77,6 @@ class BLECharacteristic {
|
||||
}
|
||||
|
||||
protected:
|
||||
bool write_event_{false};
|
||||
BLEService *service_{};
|
||||
ESPBTUUID uuid_;
|
||||
esp_gatt_char_prop_t properties_;
|
||||
|
||||
@@ -218,12 +218,19 @@ def _validate(config):
|
||||
)
|
||||
elif config[CONF_TYPE] != "OPENETH":
|
||||
if CONF_CLK_MODE in config:
|
||||
mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]]
|
||||
LOGGER.warning(
|
||||
"[ethernet] The 'clk_mode' option is deprecated and will be removed in ESPHome 2026.1. "
|
||||
"Please update your configuration to use 'clk' instead."
|
||||
"[ethernet] The 'clk_mode' option is deprecated. "
|
||||
"Please replace 'clk_mode: %s' with:\n"
|
||||
" clk:\n"
|
||||
" mode: %s\n"
|
||||
" pin: %s\n"
|
||||
"Removal scheduled for 2026.7.0.",
|
||||
config[CONF_CLK_MODE],
|
||||
mode,
|
||||
pin,
|
||||
)
|
||||
mode = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]]
|
||||
config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode[0], CONF_PIN: mode[1]})
|
||||
config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin})
|
||||
del config[CONF_CLK_MODE]
|
||||
elif CONF_CLK not in config:
|
||||
raise cv.Invalid("'clk' is a required option for [ethernet].")
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -608,8 +608,9 @@ void LD2410Component::readline_(int readch) {
|
||||
// We should never get here, but just in case...
|
||||
ESP_LOGW(TAG, "Max command length exceeded; ignoring");
|
||||
this->buffer_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
if (this->buffer_pos_ < 4) {
|
||||
if (this->buffer_pos_ < HEADER_FOOTER_SIZE) {
|
||||
return; // Not enough data to process yet
|
||||
}
|
||||
if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
|
||||
|
||||
@@ -33,8 +33,10 @@ namespace esphome::ld2410 {
|
||||
|
||||
using namespace ld24xx;
|
||||
|
||||
static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer
|
||||
static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410
|
||||
// Engineering data frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always
|
||||
// lands inside the buffer during footer-based resynchronization after losing sync.
|
||||
static constexpr uint8_t MAX_LINE_LENGTH = 50;
|
||||
static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410
|
||||
|
||||
class LD2410Component : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
|
||||
@@ -63,73 +63,73 @@ namespace esphome::ld2420 {
|
||||
static const char *const TAG = "ld2420";
|
||||
|
||||
// Local const's
|
||||
static const uint16_t REFRESH_RATE_MS = 1000;
|
||||
static constexpr uint16_t REFRESH_RATE_MS = 1000;
|
||||
|
||||
// Command sets
|
||||
static const uint16_t CMD_DISABLE_CONF = 0x00FE;
|
||||
static const uint16_t CMD_ENABLE_CONF = 0x00FF;
|
||||
static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012;
|
||||
static const uint16_t CMD_PARM_LOW_TRESH = 0x0021;
|
||||
static const uint16_t CMD_PROTOCOL_VER = 0x0002;
|
||||
static const uint16_t CMD_READ_ABD_PARAM = 0x0008;
|
||||
static const uint16_t CMD_READ_REG_ADDR = 0x0020;
|
||||
static const uint16_t CMD_READ_REGISTER = 0x0002;
|
||||
static const uint16_t CMD_READ_SERIAL_NUM = 0x0011;
|
||||
static const uint16_t CMD_READ_SYS_PARAM = 0x0013;
|
||||
static const uint16_t CMD_READ_VERSION = 0x0000;
|
||||
static const uint16_t CMD_RESTART = 0x0068;
|
||||
static const uint16_t CMD_SYSTEM_MODE = 0x0000;
|
||||
static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003;
|
||||
static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001;
|
||||
static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064;
|
||||
static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000;
|
||||
static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004;
|
||||
static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002;
|
||||
static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007;
|
||||
static const uint16_t CMD_WRITE_REGISTER = 0x0001;
|
||||
static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012;
|
||||
static constexpr uint16_t CMD_DISABLE_CONF = 0x00FE;
|
||||
static constexpr uint16_t CMD_ENABLE_CONF = 0x00FF;
|
||||
static constexpr uint16_t CMD_PARM_HIGH_TRESH = 0x0012;
|
||||
static constexpr uint16_t CMD_PARM_LOW_TRESH = 0x0021;
|
||||
static constexpr uint16_t CMD_PROTOCOL_VER = 0x0002;
|
||||
static constexpr uint16_t CMD_READ_ABD_PARAM = 0x0008;
|
||||
static constexpr uint16_t CMD_READ_REG_ADDR = 0x0020;
|
||||
static constexpr uint16_t CMD_READ_REGISTER = 0x0002;
|
||||
static constexpr uint16_t CMD_READ_SERIAL_NUM = 0x0011;
|
||||
static constexpr uint16_t CMD_READ_SYS_PARAM = 0x0013;
|
||||
static constexpr uint16_t CMD_READ_VERSION = 0x0000;
|
||||
static constexpr uint16_t CMD_RESTART = 0x0068;
|
||||
static constexpr uint16_t CMD_SYSTEM_MODE = 0x0000;
|
||||
static constexpr uint16_t CMD_SYSTEM_MODE_GR = 0x0003;
|
||||
static constexpr uint16_t CMD_SYSTEM_MODE_MTT = 0x0001;
|
||||
static constexpr uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064;
|
||||
static constexpr uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000;
|
||||
static constexpr uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004;
|
||||
static constexpr uint16_t CMD_SYSTEM_MODE_VS = 0x0002;
|
||||
static constexpr uint16_t CMD_WRITE_ABD_PARAM = 0x0007;
|
||||
static constexpr uint16_t CMD_WRITE_REGISTER = 0x0001;
|
||||
static constexpr uint16_t CMD_WRITE_SYS_PARAM = 0x0012;
|
||||
|
||||
static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04;
|
||||
static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A;
|
||||
static const uint8_t CMD_MAX_BYTES = 0x64;
|
||||
static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02;
|
||||
static constexpr uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04;
|
||||
static constexpr uint8_t CMD_ABD_DATA_REPLY_START = 0x0A;
|
||||
static constexpr uint8_t CMD_MAX_BYTES = 0x64;
|
||||
static constexpr uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02;
|
||||
|
||||
static const uint8_t LD2420_ERROR_NONE = 0x00;
|
||||
static const uint8_t LD2420_ERROR_TIMEOUT = 0x02;
|
||||
static const uint8_t LD2420_ERROR_UNKNOWN = 0x01;
|
||||
static constexpr uint8_t LD2420_ERROR_NONE = 0x00;
|
||||
static constexpr uint8_t LD2420_ERROR_TIMEOUT = 0x02;
|
||||
static constexpr uint8_t LD2420_ERROR_UNKNOWN = 0x01;
|
||||
|
||||
// Register address values
|
||||
static const uint16_t CMD_MIN_GATE_REG = 0x0000;
|
||||
static const uint16_t CMD_MAX_GATE_REG = 0x0001;
|
||||
static const uint16_t CMD_TIMEOUT_REG = 0x0004;
|
||||
static const uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015,
|
||||
0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B,
|
||||
0x001C, 0x001D, 0x001E, 0x001F};
|
||||
static const uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025,
|
||||
0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B,
|
||||
0x002C, 0x002D, 0x002E, 0x002F};
|
||||
static const uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250,
|
||||
250, 250, 250, 250, 250, 250, 250, 250};
|
||||
static const uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150,
|
||||
150, 100, 100, 100, 100, 100, 100, 100};
|
||||
static const uint16_t FACTORY_TIMEOUT = 120;
|
||||
static const uint16_t FACTORY_MIN_GATE = 1;
|
||||
static const uint16_t FACTORY_MAX_GATE = 12;
|
||||
static constexpr uint16_t CMD_MIN_GATE_REG = 0x0000;
|
||||
static constexpr uint16_t CMD_MAX_GATE_REG = 0x0001;
|
||||
static constexpr uint16_t CMD_TIMEOUT_REG = 0x0004;
|
||||
static constexpr uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015,
|
||||
0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B,
|
||||
0x001C, 0x001D, 0x001E, 0x001F};
|
||||
static constexpr uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025,
|
||||
0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B,
|
||||
0x002C, 0x002D, 0x002E, 0x002F};
|
||||
static constexpr uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250,
|
||||
250, 250, 250, 250, 250, 250, 250, 250};
|
||||
static constexpr uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150,
|
||||
150, 100, 100, 100, 100, 100, 100, 100};
|
||||
static constexpr uint16_t FACTORY_TIMEOUT = 120;
|
||||
static constexpr uint16_t FACTORY_MIN_GATE = 1;
|
||||
static constexpr uint16_t FACTORY_MAX_GATE = 12;
|
||||
|
||||
// COMMAND_BYTE Header & Footer
|
||||
static const uint32_t CMD_FRAME_FOOTER = 0x01020304;
|
||||
static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD;
|
||||
static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD;
|
||||
static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA;
|
||||
static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8;
|
||||
static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4;
|
||||
static const int CALIBRATE_VERSION_MIN = 154;
|
||||
static const uint8_t CMD_FRAME_COMMAND = 6;
|
||||
static const uint8_t CMD_FRAME_DATA_LENGTH = 4;
|
||||
static const uint8_t CMD_FRAME_STATUS = 7;
|
||||
static const uint8_t CMD_ERROR_WORD = 8;
|
||||
static const uint8_t ENERGY_SENSOR_START = 9;
|
||||
static const uint8_t CALIBRATE_REPORT_INTERVAL = 4;
|
||||
static constexpr uint32_t CMD_FRAME_FOOTER = 0x01020304;
|
||||
static constexpr uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD;
|
||||
static constexpr uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD;
|
||||
static constexpr uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA;
|
||||
static constexpr uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8;
|
||||
static constexpr uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4;
|
||||
static constexpr int CALIBRATE_VERSION_MIN = 154;
|
||||
static constexpr uint8_t CMD_FRAME_COMMAND = 6;
|
||||
static constexpr uint8_t CMD_FRAME_DATA_LENGTH = 4;
|
||||
static constexpr uint8_t CMD_FRAME_STATUS = 7;
|
||||
static constexpr uint8_t CMD_ERROR_WORD = 8;
|
||||
static constexpr uint8_t ENERGY_SENSOR_START = 9;
|
||||
static constexpr uint8_t CALIBRATE_REPORT_INTERVAL = 4;
|
||||
static const char *const OP_NORMAL_MODE_STRING = "Normal";
|
||||
static const char *const OP_SIMPLE_MODE_STRING = "Simple";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
|
||||
namespace esphome::ld2420 {
|
||||
|
||||
static const uint8_t CALIBRATE_SAMPLES = 64;
|
||||
static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer
|
||||
static const uint8_t TOTAL_GATES = 16;
|
||||
static constexpr uint8_t CALIBRATE_SAMPLES = 64;
|
||||
// Energy frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always lands
|
||||
// inside the buffer during footer-based resynchronization after losing sync.
|
||||
static constexpr uint8_t MAX_LINE_LENGTH = 50;
|
||||
static constexpr uint8_t TOTAL_GATES = 16;
|
||||
|
||||
enum OpMode : uint8_t {
|
||||
OP_NORMAL_MODE = 1,
|
||||
|
||||
@@ -776,8 +776,9 @@ void LD2450Component::readline_(int readch) {
|
||||
// We should never get here, but just in case...
|
||||
ESP_LOGW(TAG, "Max command length exceeded; ignoring");
|
||||
this->buffer_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
if (this->buffer_pos_ < 4) {
|
||||
if (this->buffer_pos_ < HEADER_FOOTER_SIZE) {
|
||||
return; // Not enough data to process yet
|
||||
}
|
||||
if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] &&
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/component.h"
|
||||
#ifdef USE_SENSOR
|
||||
@@ -37,9 +38,11 @@ using namespace ld24xx;
|
||||
|
||||
// Constants
|
||||
static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec.
|
||||
static constexpr uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer
|
||||
static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450
|
||||
static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450
|
||||
// Zone query response is 40 bytes; +1 for null terminator, +4 so that a frame footer always
|
||||
// lands inside the buffer during footer-based resynchronization after losing sync.
|
||||
static constexpr uint8_t MAX_LINE_LENGTH = 45;
|
||||
static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450
|
||||
static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450
|
||||
|
||||
enum Direction : uint8_t {
|
||||
DIRECTION_APPROACHING = 0,
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef HAS_PCNT
|
||||
#include <esp_clk_tree.h>
|
||||
#include <esp_private/esp_clk.h>
|
||||
#include <hal/pcnt_ll.h>
|
||||
#endif
|
||||
|
||||
@@ -117,9 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) {
|
||||
}
|
||||
|
||||
if (this->filter_us != 0) {
|
||||
uint32_t apb_freq;
|
||||
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq);
|
||||
uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq;
|
||||
uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000u / ((uint32_t) esp_clk_apb_freq() / 1000000u);
|
||||
pcnt_glitch_filter_config_t filter_config = {
|
||||
.max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns),
|
||||
};
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
#if defined(USE_ESP32)
|
||||
#include <soc/soc_caps.h>
|
||||
#ifdef SOC_PCNT_SUPPORTED
|
||||
#if defined(SOC_PCNT_SUPPORTED) && __has_include(<driver/pulse_cnt.h>)
|
||||
#include <driver/pulse_cnt.h>
|
||||
#define HAS_PCNT
|
||||
#endif // SOC_PCNT_SUPPORTED
|
||||
#endif // defined(SOC_PCNT_SUPPORTED) && __has_include(<driver/pulse_cnt.h>)
|
||||
#endif // USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_system.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::safe_mode {
|
||||
@@ -54,6 +55,10 @@ void SafeModeComponent::dump_config() {
|
||||
"OTA rollback detected! Rolled back from partition '%s'\n"
|
||||
"The device reset before the boot was marked successful",
|
||||
last_invalid->label);
|
||||
if (esp_reset_reason() == ESP_RST_BROWNOUT) {
|
||||
ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n"
|
||||
"See https://esphome.io/guides/faq.html#brownout-detector-was-triggered");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -59,8 +59,14 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s
|
||||
#if USE_NETWORK_IPV6
|
||||
else if (addr_ptr->sa_family == AF_INET6 && len >= sizeof(sockaddr_in6)) {
|
||||
const auto *addr = reinterpret_cast<const struct sockaddr_in6 *>(addr_ptr);
|
||||
#ifndef USE_SOCKET_IMPL_LWIP_TCP
|
||||
// Format IPv4-mapped IPv6 addresses as regular IPv4 (not supported on ESP8266 raw TCP)
|
||||
#ifdef USE_HOST
|
||||
// Format IPv4-mapped IPv6 addresses as regular IPv4 (POSIX layout, no LWIP union)
|
||||
if (IN6_IS_ADDR_V4MAPPED(&addr->sin6_addr) &&
|
||||
esphome_inet_ntop4(&addr->sin6_addr.s6_addr[12], buf.data(), buf.size()) != nullptr) {
|
||||
return strlen(buf.data());
|
||||
}
|
||||
#elif !defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||
// Format IPv4-mapped IPv6 addresses as regular IPv4 (LWIP layout)
|
||||
if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 &&
|
||||
addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) &&
|
||||
esphome_inet_ntop4(&addr->sin6_addr.un.u32_addr[3], buf.data(), buf.size()) != nullptr) {
|
||||
|
||||
@@ -19,6 +19,13 @@ 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) {
|
||||
@@ -150,20 +157,26 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
// 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 (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) {
|
||||
if (is_default_uart0_pin(tx)) {
|
||||
gpio_reset_pin(static_cast<gpio_num_t>(tx));
|
||||
}
|
||||
if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) {
|
||||
if (is_default_uart0_pin(rx)) {
|
||||
gpio_reset_pin(static_cast<gpio_num_t>(rx));
|
||||
}
|
||||
|
||||
// Setup pins after reset to preserve open drain/pullup/pulldown flags
|
||||
// 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 ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
|
||||
if (is_default_uart0_pin(pin->get_pin()) || (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
|
||||
pin->setup();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DEPENDENCIES = ["network"]
|
||||
@@ -65,33 +66,47 @@ RELOCATED = {
|
||||
)
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(UDPComponent),
|
||||
cv.Optional(CONF_PORT, default=18511): cv.Any(
|
||||
cv.port,
|
||||
cv.Schema(
|
||||
|
||||
def _consume_udp_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for UDP component."""
|
||||
from esphome.components import socket
|
||||
|
||||
# UDP uses up to 2 sockets: 1 broadcast + 1 listen
|
||||
# Whether each is used depends on code generation, so register worst case
|
||||
socket.consume_sockets(2, "udp")(config)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(UDPComponent),
|
||||
cv.Optional(CONF_PORT, default=18511): cv.Any(
|
||||
cv.port,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_LISTEN_PORT): cv.port,
|
||||
cv.Required(CONF_BROADCAST_PORT): cv.port,
|
||||
}
|
||||
),
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_LISTEN_ADDRESS, default="255.255.255.255"
|
||||
): cv.ipv4address_multi_broadcast,
|
||||
cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list(
|
||||
cv.ipv4address,
|
||||
),
|
||||
cv.Optional(CONF_ON_RECEIVE): automation.validate_automation(
|
||||
{
|
||||
cv.Required(CONF_LISTEN_PORT): cv.port,
|
||||
cv.Required(CONF_BROADCAST_PORT): cv.port,
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
Trigger.template(trigger_args)
|
||||
),
|
||||
}
|
||||
),
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_LISTEN_ADDRESS, default="255.255.255.255"
|
||||
): cv.ipv4address_multi_broadcast,
|
||||
cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list(
|
||||
cv.ipv4address,
|
||||
),
|
||||
cv.Optional(CONF_ON_RECEIVE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
Trigger.template(trigger_args)
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
).extend(RELOCATED)
|
||||
}
|
||||
).extend(RELOCATED),
|
||||
_consume_udp_sockets,
|
||||
)
|
||||
|
||||
|
||||
async def register_udp_client(var, config):
|
||||
|
||||
@@ -144,9 +144,10 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for web_server component."""
|
||||
from esphome.components import socket
|
||||
|
||||
# Web server needs 1 listening socket + typically 2 concurrent client connections
|
||||
# (browser makes 2 connections for page + event stream)
|
||||
sockets_needed = 3
|
||||
# Web server needs 1 listening socket + typically 5 concurrent client connections
|
||||
# (browser opens connections for page resources, SSE event stream, and POST
|
||||
# requests for entity control which may linger before closing)
|
||||
sockets_needed = 6
|
||||
socket.consume_sockets(sockets_needed, "web_server")(config)
|
||||
return config
|
||||
|
||||
|
||||
@@ -1913,6 +1913,9 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe
|
||||
JsonArray modes = root[ESPHOME_F("modes")].to<JsonArray>();
|
||||
for (auto m : traits.get_supported_modes())
|
||||
modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m)));
|
||||
root[ESPHOME_F("min_temp")] = traits.get_min_temperature();
|
||||
root[ESPHOME_F("max_temp")] = traits.get_max_temperature();
|
||||
root[ESPHOME_F("step")] = traits.get_target_temperature_step();
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
|
||||
@@ -1935,10 +1938,6 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe
|
||||
root[ESPHOME_F("target_temperature")] = target;
|
||||
}
|
||||
|
||||
root[ESPHOME_F("min_temperature")] = traits.get_min_temperature();
|
||||
root[ESPHOME_F("max_temperature")] = traits.get_max_temperature();
|
||||
root[ESPHOME_F("step")] = traits.get_target_temperature_step();
|
||||
|
||||
if (traits.get_supports_away_mode()) {
|
||||
root[ESPHOME_F("away")] = obj->is_away();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import math
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
@@ -493,6 +494,13 @@ async def to_code(config):
|
||||
cg.add(var.set_passive_scan(True))
|
||||
if CONF_OUTPUT_POWER in config:
|
||||
cg.add(var.set_output_power(config[CONF_OUTPUT_POWER]))
|
||||
if CORE.is_esp32:
|
||||
# Set PHY max TX power to match output_power so calibration also uses
|
||||
# reduced power. This prevents brownout during PHY init on marginal
|
||||
# power supplies, which is critical for OTA updates with rollback enabled.
|
||||
# Kconfig range is 10-20, ESPHome allows 8.5-20.5
|
||||
phy_tx_power = max(10, min(20, math.ceil(config[CONF_OUTPUT_POWER])))
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_PHY_MAX_WIFI_TX_POWER", phy_tx_power)
|
||||
# enable_on_boot defaults to true in C++ - only set if false
|
||||
if not config[CONF_ENABLE_ON_BOOT]:
|
||||
cg.add(var.set_enable_on_boot(False))
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.2.0"
|
||||
__version__ = "2026.2.2"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -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!");
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<<: !include common.yaml
|
||||
|
||||
esp32_ble:
|
||||
io_capability: keyboard_only
|
||||
disable_bt_logs: false
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include "esphome/components/ld2450/ld2450.h"
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
|
||||
namespace esphome::ld2450::testing {
|
||||
|
||||
// Mock UART component to satisfy UARTDevice parent requirement.
|
||||
class MockUARTComponent : public uart::UARTComponent {
|
||||
public:
|
||||
void write_array(const uint8_t *data, size_t len) override {}
|
||||
MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override));
|
||||
MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override));
|
||||
MOCK_METHOD(size_t, available, (), (override));
|
||||
MOCK_METHOD(void, flush, (), (override));
|
||||
MOCK_METHOD(void, check_logger_conflict, (), (override));
|
||||
};
|
||||
|
||||
// Expose protected members for testing.
|
||||
class TestableLD2450 : public LD2450Component {
|
||||
public:
|
||||
using LD2450Component::buffer_data_;
|
||||
using LD2450Component::buffer_pos_;
|
||||
using LD2450Component::readline_;
|
||||
|
||||
void feed(const std::vector<uint8_t> &data) {
|
||||
for (uint8_t byte : data) {
|
||||
this->readline_(byte);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// LD2450 periodic data frame: header (4) + 3 targets * 8 bytes + footer (2) = 30 bytes
|
||||
// All-zero targets means no presence detected.
|
||||
inline std::vector<uint8_t> make_periodic_frame(uint8_t fill = 0x00) {
|
||||
std::vector<uint8_t> frame = {0xAA, 0xFF, 0x03, 0x00}; // DATA_FRAME_HEADER
|
||||
for (int i = 0; i < 24; i++) {
|
||||
frame.push_back(fill); // 3 targets * 8 bytes
|
||||
}
|
||||
frame.push_back(0x55); // DATA_FRAME_FOOTER
|
||||
frame.push_back(0xCC);
|
||||
return frame;
|
||||
}
|
||||
|
||||
// LD2450 command ACK frame for CMD_ENABLE_CONF (0xFF), successful.
|
||||
// header (4) + length (2) + command (2) + result (2) + footer (4) = 14 bytes
|
||||
inline std::vector<uint8_t> make_ack_frame() {
|
||||
return {
|
||||
0xFD, 0xFC, 0xFB, 0xFA, // CMD_FRAME_HEADER
|
||||
0x04, 0x00, // length = 4
|
||||
0xFF, 0x01, // command = enable_conf, status = success
|
||||
0x00, 0x00, // result = ok
|
||||
0x04, 0x03, 0x02, 0x01 // CMD_FRAME_FOOTER
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2450::testing
|
||||
@@ -0,0 +1,145 @@
|
||||
#include "common.h"
|
||||
|
||||
namespace esphome::ld2450::testing {
|
||||
|
||||
class LD2450ReadlineTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->ld2450_.set_uart_parent(&this->mock_uart_);
|
||||
// Ensure clean state
|
||||
ASSERT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
}
|
||||
|
||||
MockUARTComponent mock_uart_;
|
||||
TestableLD2450 ld2450_;
|
||||
};
|
||||
|
||||
// --- Good data tests ---
|
||||
|
||||
TEST_F(LD2450ReadlineTest, ValidPeriodicFrame) {
|
||||
auto frame = make_periodic_frame();
|
||||
this->ld2450_.feed(frame);
|
||||
// After a complete valid frame, buffer should be reset
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
}
|
||||
|
||||
TEST_F(LD2450ReadlineTest, ValidCommandAckFrame) {
|
||||
auto frame = make_ack_frame();
|
||||
this->ld2450_.feed(frame);
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
}
|
||||
|
||||
TEST_F(LD2450ReadlineTest, BackToBackPeriodicFrames) {
|
||||
auto frame = make_periodic_frame();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
this->ld2450_.feed(frame);
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Frame " << i << " not processed";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) {
|
||||
auto periodic = make_periodic_frame();
|
||||
auto ack = make_ack_frame();
|
||||
this->ld2450_.feed(periodic);
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
this->ld2450_.feed(ack);
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
this->ld2450_.feed(periodic);
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
}
|
||||
|
||||
// --- Garbage then valid frame tests ---
|
||||
|
||||
TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) {
|
||||
// Garbage bytes accumulate in the buffer but don't match any footer.
|
||||
// A valid frame follows; its footer resets the buffer and resyncs.
|
||||
std::vector<uint8_t> garbage = {0x01, 0x02, 0x03, 0x42, 0x99};
|
||||
this->ld2450_.feed(garbage);
|
||||
EXPECT_GT(this->ld2450_.buffer_pos_, 0); // Garbage accumulated
|
||||
|
||||
auto frame = make_periodic_frame();
|
||||
this->ld2450_.feed(frame);
|
||||
// Footer from the valid frame resyncs the parser
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
}
|
||||
|
||||
// --- Footer-based resynchronization tests ---
|
||||
|
||||
TEST_F(LD2450ReadlineTest, FooterInGarbageResyncs) {
|
||||
// Garbage containing a periodic frame footer (0x55 0xCC) triggers
|
||||
// a buffer reset, allowing the next frame to be parsed cleanly.
|
||||
std::vector<uint8_t> garbage_with_footer = {0x01, 0x02, 0x03, 0x04, 0x55, 0xCC};
|
||||
this->ld2450_.feed(garbage_with_footer);
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0); // Footer reset the buffer
|
||||
|
||||
auto frame = make_periodic_frame();
|
||||
this->ld2450_.feed(frame);
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
}
|
||||
|
||||
TEST_F(LD2450ReadlineTest, CmdFooterInGarbageResyncs) {
|
||||
// Garbage containing a command frame footer (04 03 02 01) also resyncs.
|
||||
std::vector<uint8_t> garbage_with_footer = {0x10, 0x20, 0x30, 0x40, 0x04, 0x03, 0x02, 0x01};
|
||||
this->ld2450_.feed(garbage_with_footer);
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
|
||||
auto frame = make_periodic_frame();
|
||||
this->ld2450_.feed(frame);
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
}
|
||||
|
||||
// --- Overflow recovery tests ---
|
||||
|
||||
TEST_F(LD2450ReadlineTest, OverflowResetsBuffer) {
|
||||
// Fill the buffer to capacity with filler that won't match any footer.
|
||||
// MAX_LINE_LENGTH is 45, usable is 44. The 45th byte triggers overflow.
|
||||
std::vector<uint8_t> overflow_data(MAX_LINE_LENGTH, 0x11);
|
||||
this->ld2450_.feed(overflow_data);
|
||||
// After overflow, buffer_pos_ resets to 0 (via the < 4 early return path)
|
||||
EXPECT_LT(this->ld2450_.buffer_pos_, 4);
|
||||
}
|
||||
|
||||
TEST_F(LD2450ReadlineTest, OverflowThenValidFrame) {
|
||||
// Overflow, then a valid frame should be processed.
|
||||
std::vector<uint8_t> overflow_data(MAX_LINE_LENGTH, 0x11);
|
||||
this->ld2450_.feed(overflow_data);
|
||||
|
||||
auto frame = make_periodic_frame();
|
||||
this->ld2450_.feed(frame);
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
}
|
||||
|
||||
TEST_F(LD2450ReadlineTest, BufferLargeEnoughForDesyncedFooter) {
|
||||
// The key fix: the buffer (45) is large enough that a desynced periodic frame's
|
||||
// footer (at most 30 bytes into the stream) will land inside the buffer before overflow.
|
||||
// Simulate starting 10 bytes into a periodic frame, then a full frame follows.
|
||||
std::vector<uint8_t> mid_frame = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39};
|
||||
// Then a complete periodic frame whose footer will land at position 40 (10 + 30),
|
||||
// well within the buffer size of 45.
|
||||
auto frame = make_periodic_frame();
|
||||
mid_frame.insert(mid_frame.end(), frame.begin(), frame.end());
|
||||
|
||||
this->ld2450_.feed(mid_frame);
|
||||
// The footer from the frame should have triggered a reset
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
}
|
||||
|
||||
TEST_F(LD2450ReadlineTest, SimulatedRestartThenFrames) {
|
||||
// Simulate LD2450 restart: burst of garbage followed by valid periodic frames.
|
||||
// The garbage + first frame should fit in the buffer so the footer resyncs.
|
||||
std::vector<uint8_t> restart_noise = {
|
||||
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 8 bytes of mid-frame data
|
||||
};
|
||||
auto frame = make_periodic_frame();
|
||||
// 8 garbage + 30 frame = 38 bytes, well within buffer of 45
|
||||
restart_noise.insert(restart_noise.end(), frame.begin(), frame.end());
|
||||
|
||||
this->ld2450_.feed(restart_noise);
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
|
||||
// Subsequent frames should work normally
|
||||
this->ld2450_.feed(frame);
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2450::testing
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
sensor:
|
||||
- platform: pulse_counter
|
||||
name: Pulse Counter
|
||||
pin: 4
|
||||
use_pcnt: false
|
||||
count_mode:
|
||||
rising_edge: INCREMENT
|
||||
falling_edge: DECREMENT
|
||||
internal_filter: 13us
|
||||
update_interval: 15s
|
||||
@@ -0,0 +1,11 @@
|
||||
substitutions:
|
||||
network_enable_ipv6: "false"
|
||||
|
||||
socket:
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
network:
|
||||
enable_ipv6: ${network_enable_ipv6}
|
||||
@@ -0,0 +1,4 @@
|
||||
substitutions:
|
||||
network_enable_ipv6: "true"
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
socket:
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
socket:
|
||||
|
||||
network:
|
||||
@@ -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