mirror of
https://github.com/esphome/esphome.git
synced 2026-09-17 01:58:39 +00:00
Merge remote-tracking branch 'upstream/dev' into wifi_roam
This commit is contained in:
@@ -25,11 +25,13 @@ class AddressableLightDisplay : public display::DisplayBuffer {
|
||||
if (enabled_ && !enabled) { // enabled -> disabled
|
||||
// - Tell the parent light to refresh, effectively wiping the display. Also
|
||||
// restores the previous effect (if any).
|
||||
light_state_->make_call().set_effect(this->last_effect_).perform();
|
||||
if (this->last_effect_index_.has_value()) {
|
||||
light_state_->make_call().set_effect(*this->last_effect_index_).perform();
|
||||
}
|
||||
|
||||
} else if (!enabled_ && enabled) { // disabled -> enabled
|
||||
// - Save the current effect.
|
||||
this->last_effect_ = light_state_->get_effect_name();
|
||||
// - Save the current effect index.
|
||||
this->last_effect_index_ = light_state_->get_current_effect_index();
|
||||
// - Disable any current effect.
|
||||
light_state_->make_call().set_effect(0).perform();
|
||||
}
|
||||
@@ -56,7 +58,7 @@ class AddressableLightDisplay : public display::DisplayBuffer {
|
||||
int32_t width_;
|
||||
int32_t height_;
|
||||
std::vector<Color> addressable_light_buffer_;
|
||||
optional<std::string> last_effect_;
|
||||
optional<uint32_t> last_effect_index_;
|
||||
optional<std::function<int(int, int)>> pixel_mapper_f_;
|
||||
};
|
||||
} // namespace addressable_light
|
||||
|
||||
@@ -2425,7 +2425,7 @@ message ZWaveProxyFrame {
|
||||
option (ifdef) = "USE_ZWAVE_PROXY";
|
||||
option (no_delay) = true;
|
||||
|
||||
bytes data = 1 [(pointer_to_buffer) = true];
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
enum ZWaveProxyRequestType {
|
||||
@@ -2439,5 +2439,5 @@ message ZWaveProxyRequest {
|
||||
option (ifdef) = "USE_ZWAVE_PROXY";
|
||||
|
||||
ZWaveProxyRequestType type = 1;
|
||||
bytes data = 2 [(pointer_to_buffer) = true];
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
@@ -1530,7 +1530,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 13;
|
||||
resp.api_version_minor = 14;
|
||||
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
||||
resp.set_server_info(ESPHOME_VERSION_REF);
|
||||
resp.set_name(StringRef(App.get_name()));
|
||||
|
||||
@@ -24,9 +24,10 @@ struct ClientInfo {
|
||||
// Keepalive timeout in milliseconds
|
||||
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
|
||||
// Maximum number of entities to process in a single batch during initial state/info sending
|
||||
// This was increased from 20 to 24 after removing the unique_id field from entity info messages,
|
||||
// which reduced message sizes allowing more entities per batch without exceeding packet limits
|
||||
static constexpr size_t MAX_INITIAL_PER_BATCH = 24;
|
||||
// API 1.14+ clients compute object_id client-side, so messages are smaller and we can fit more per batch
|
||||
// TODO: Remove MAX_INITIAL_PER_BATCH_LEGACY before 2026.7.0 - all clients should support API 1.14 by then
|
||||
static constexpr size_t MAX_INITIAL_PER_BATCH_LEGACY = 24; // For clients < API 1.14 (includes object_id)
|
||||
static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= API 1.14 (no object_id)
|
||||
// Maximum number of packets to process in a single batch (platform-dependent)
|
||||
// This limit exists to prevent stack overflow from the PacketInfo array in process_batch_
|
||||
// Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes
|
||||
@@ -323,10 +324,16 @@ class APIConnection final : public APIServerConnection {
|
||||
APIConnection *conn, uint32_t remaining_size, bool is_single) {
|
||||
// Set common fields that are shared by all entity types
|
||||
msg.key = entity->get_object_id_hash();
|
||||
// Get object_id with zero heap allocation
|
||||
// Static case returns direct reference, dynamic case uses buffer
|
||||
|
||||
// API 1.14+ clients compute object_id client-side from the entity name
|
||||
// For older clients, we must send object_id for backward compatibility
|
||||
// See: https://github.com/esphome/backlog/issues/76
|
||||
// TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then
|
||||
// Buffer must remain in scope until encode_message_to_buffer is called
|
||||
char object_id_buf[OBJECT_ID_MAX_LEN];
|
||||
msg.set_object_id(entity->get_object_id_to(object_id_buf));
|
||||
if (!conn->client_supports_api_version(1, 14)) {
|
||||
msg.set_object_id(entity->get_object_id_to(object_id_buf));
|
||||
}
|
||||
|
||||
if (entity->has_own_name()) {
|
||||
msg.set_name(entity->get_name());
|
||||
@@ -349,16 +356,24 @@ class APIConnection final : public APIServerConnection {
|
||||
inline bool check_voice_assistant_api_connection_() const;
|
||||
#endif
|
||||
|
||||
// Get the max batch size based on client API version
|
||||
// API 1.14+ clients don't receive object_id, so messages are smaller and more fit per batch
|
||||
// TODO: Remove this method before 2026.7.0 and use MAX_INITIAL_PER_BATCH directly
|
||||
size_t get_max_batch_size_() const {
|
||||
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
|
||||
}
|
||||
|
||||
// Helper method to process multiple entities from an iterator in a batch
|
||||
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < MAX_INITIAL_PER_BATCH) {
|
||||
size_t max_batch = this->get_max_batch_size_();
|
||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
|
||||
iterator.advance();
|
||||
}
|
||||
|
||||
// If the batch is full, process it immediately
|
||||
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
||||
if (this->deferred_batch_.size() >= MAX_INITIAL_PER_BATCH) {
|
||||
if (this->deferred_batch_.size() >= max_batch) {
|
||||
this->process_batch_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,26 @@ namespace esphome::api {
|
||||
|
||||
static const char *const TAG = "api.frame_helper";
|
||||
|
||||
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
|
||||
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
|
||||
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
|
||||
#define LOG_PACKET_RECEIVED(buffer) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Received frame: %s", \
|
||||
format_hex_pretty_to(hex_buf_, (buffer).data(), \
|
||||
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#define LOG_PACKET_SENDING(data, len) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Sending raw: %s", \
|
||||
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#else
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
||||
|
||||
@@ -24,12 +24,26 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit";
|
||||
#endif
|
||||
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
|
||||
|
||||
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
|
||||
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
|
||||
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
|
||||
#define LOG_PACKET_RECEIVED(buffer) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Received frame: %s", \
|
||||
format_hex_pretty_to(hex_buf_, (buffer).data(), \
|
||||
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#define LOG_PACKET_SENDING(data, len) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Sending raw: %s", \
|
||||
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#else
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
||||
|
||||
@@ -18,12 +18,26 @@ namespace esphome::api {
|
||||
|
||||
static const char *const TAG = "api.plaintext";
|
||||
|
||||
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
|
||||
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
|
||||
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
|
||||
#define LOG_PACKET_RECEIVED(buffer) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Received frame: %s", \
|
||||
format_hex_pretty_to(hex_buf_, (buffer).data(), \
|
||||
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#define LOG_PACKET_SENDING(data, len) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Sending raw: %s", \
|
||||
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#else
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
||||
|
||||
@@ -1046,7 +1046,7 @@ class SubscribeLogsRequest final : public ProtoDecodableMessage {
|
||||
class SubscribeLogsResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 29;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 11;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 21;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "subscribe_logs_response"; }
|
||||
#endif
|
||||
@@ -1069,7 +1069,7 @@ class SubscribeLogsResponse final : public ProtoMessage {
|
||||
class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 124;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 9;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "noise_encryption_set_key_request"; }
|
||||
#endif
|
||||
@@ -1161,7 +1161,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
|
||||
class HomeassistantActionResponse final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 130;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 24;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 34;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "homeassistant_action_response"; }
|
||||
#endif
|
||||
@@ -1388,7 +1388,7 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage {
|
||||
class CameraImageResponse final : public StateResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 44;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 20;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 30;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "camera_image_response"; }
|
||||
#endif
|
||||
@@ -2123,7 +2123,7 @@ class BluetoothGATTReadRequest final : public ProtoDecodableMessage {
|
||||
class BluetoothGATTReadResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 74;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 17;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 27;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "bluetooth_gatt_read_response"; }
|
||||
#endif
|
||||
@@ -2146,7 +2146,7 @@ class BluetoothGATTReadResponse final : public ProtoMessage {
|
||||
class BluetoothGATTWriteRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 75;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 29;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "bluetooth_gatt_write_request"; }
|
||||
#endif
|
||||
@@ -2182,7 +2182,7 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage {
|
||||
class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 77;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 17;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 27;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; }
|
||||
#endif
|
||||
@@ -2218,7 +2218,7 @@ class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage {
|
||||
class BluetoothGATTNotifyDataResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 79;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 17;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 27;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "bluetooth_gatt_notify_data_response"; }
|
||||
#endif
|
||||
|
||||
@@ -128,7 +128,9 @@ void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->
|
||||
bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; }
|
||||
|
||||
void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); }
|
||||
std::string CH422GGPIOPin::dump_summary() const { return str_sprintf("EXIO%u via CH422G", pin_); }
|
||||
size_t CH422GGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "EXIO%u via CH422G", this->pin_);
|
||||
}
|
||||
void CH422GGPIOPin::set_flags(gpio::Flags flags) {
|
||||
flags_ = flags;
|
||||
this->parent_->pin_mode(this->pin_, flags);
|
||||
|
||||
@@ -50,7 +50,7 @@ class CH422GGPIOPin : public GPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_parent(CH422GComponent *parent) { parent_ = parent; }
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
|
||||
@@ -97,10 +97,8 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
|
||||
gpio_isr_handler_add(this->get_pin_num(), func, arg);
|
||||
}
|
||||
|
||||
std::string ESP32InternalGPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "GPIO%" PRIu32, static_cast<uint32_t>(this->pin_));
|
||||
return buffer;
|
||||
size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "GPIO%" PRIu32, static_cast<uint32_t>(this->pin_));
|
||||
}
|
||||
|
||||
void ESP32InternalGPIOPin::setup() {
|
||||
|
||||
@@ -24,7 +24,7 @@ class ESP32InternalGPIOPin : public InternalGPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return this->pin_; }
|
||||
|
||||
@@ -3,7 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, esp32_ble, improv_base, output
|
||||
from esphome.components.esp32_ble import BTLoggers
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
|
||||
from esphome.const import CONF_ID, CONF_ON_START, CONF_ON_STATE, CONF_TRIGGER_ID
|
||||
|
||||
AUTO_LOAD = ["esp32_ble_server", "improv_base"]
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
@@ -15,7 +15,6 @@ CONF_BLE_SERVER_ID = "ble_server_id"
|
||||
CONF_IDENTIFY_DURATION = "identify_duration"
|
||||
CONF_ON_PROVISIONED = "on_provisioned"
|
||||
CONF_ON_PROVISIONING = "on_provisioning"
|
||||
CONF_ON_START = "on_start"
|
||||
CONF_ON_STOP = "on_stop"
|
||||
CONF_STATUS_INDICATOR = "status_indicator"
|
||||
CONF_WIFI_TIMEOUT = "wifi_timeout"
|
||||
|
||||
@@ -28,6 +28,7 @@ from .const import (
|
||||
KEY_ESP8266,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_PIN_INITIAL_STATES,
|
||||
KEY_WAVEFORM_REQUIRED,
|
||||
esp8266_ns,
|
||||
)
|
||||
from .gpio import PinInitialState, add_pin_initial_states_array
|
||||
@@ -192,7 +193,12 @@ async def to_code(config):
|
||||
|
||||
cg.add_platformio_option(
|
||||
"extra_scripts",
|
||||
["pre:testing_mode.py", "pre:exclude_updater.py", "post:post_build.py"],
|
||||
[
|
||||
"pre:testing_mode.py",
|
||||
"pre:exclude_updater.py",
|
||||
"pre:exclude_waveform.py",
|
||||
"post:post_build.py",
|
||||
],
|
||||
)
|
||||
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
@@ -264,10 +270,24 @@ async def to_code(config):
|
||||
cg.add_platformio_option("board_build.ldscript", ld_script)
|
||||
|
||||
CORE.add_job(add_pin_initial_states_array)
|
||||
CORE.add_job(finalize_waveform_config)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.WORKAROUNDS)
|
||||
async def finalize_waveform_config() -> None:
|
||||
"""Add waveform stubs define if waveform is not required.
|
||||
|
||||
This runs at WORKAROUNDS priority (-999) to ensure all components
|
||||
have had a chance to call require_waveform() first.
|
||||
"""
|
||||
if not CORE.data.get(KEY_ESP8266, {}).get(KEY_WAVEFORM_REQUIRED, False):
|
||||
# No component needs waveform - enable stubs and exclude Arduino waveform code
|
||||
# Use build flag (visible to both C++ code and PlatformIO script)
|
||||
cg.add_build_flag("-DUSE_ESP8266_WAVEFORM_STUBS")
|
||||
|
||||
|
||||
# Called by writer.py
|
||||
def copy_files():
|
||||
def copy_files() -> None:
|
||||
dir = Path(__file__).parent
|
||||
post_build_file = dir / "post_build.py.script"
|
||||
copy_file_if_changed(
|
||||
@@ -284,3 +304,8 @@ def copy_files():
|
||||
exclude_updater_file,
|
||||
CORE.relative_build_path("exclude_updater.py"),
|
||||
)
|
||||
exclude_waveform_file = dir / "exclude_waveform.py.script"
|
||||
copy_file_if_changed(
|
||||
exclude_waveform_file,
|
||||
CORE.relative_build_path("exclude_waveform.py"),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.core import CORE
|
||||
|
||||
KEY_ESP8266 = "esp8266"
|
||||
KEY_BOARD = "board"
|
||||
@@ -6,6 +7,25 @@ KEY_PIN_INITIAL_STATES = "pin_initial_states"
|
||||
CONF_RESTORE_FROM_FLASH = "restore_from_flash"
|
||||
CONF_EARLY_PIN_INIT = "early_pin_init"
|
||||
KEY_FLASH_SIZE = "flash_size"
|
||||
KEY_WAVEFORM_REQUIRED = "waveform_required"
|
||||
|
||||
# esp8266 namespace is already defined by arduino, manually prefix esphome
|
||||
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
|
||||
|
||||
|
||||
def require_waveform() -> None:
|
||||
"""Mark that Arduino waveform/PWM support is required.
|
||||
|
||||
Call this from components that need the Arduino waveform generator
|
||||
(startWaveform, stopWaveform, analogWrite, Tone, Servo).
|
||||
|
||||
If no component calls this, the waveform code is excluded from the build
|
||||
to save ~596 bytes of RAM and 464 bytes of flash.
|
||||
|
||||
Example:
|
||||
from esphome.components.esp8266.const import require_waveform
|
||||
|
||||
async def to_code(config):
|
||||
require_waveform()
|
||||
"""
|
||||
CORE.data.setdefault(KEY_ESP8266, {})[KEY_WAVEFORM_REQUIRED] = True
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# pylint: disable=E0602
|
||||
Import("env") # noqa
|
||||
|
||||
import os
|
||||
|
||||
# Filter out waveform/PWM code from the Arduino core build
|
||||
# This saves ~596 bytes of RAM and 464 bytes of flash by not
|
||||
# instantiating the waveform generator state structures (wvfState + pwmState).
|
||||
#
|
||||
# The waveform code is used by: analogWrite, Tone, Servo, and direct
|
||||
# startWaveform/stopWaveform calls. ESPHome's esp8266_pwm component
|
||||
# calls require_waveform() to keep this code when needed.
|
||||
#
|
||||
# When excluded, we provide stub implementations of stopWaveform() and
|
||||
# _stopPWM() since digitalWrite() calls these unconditionally.
|
||||
|
||||
|
||||
def has_define_flag(env, name):
|
||||
"""Check if a define exists in the build flags."""
|
||||
define_flag = f"-D{name}"
|
||||
# Check BUILD_FLAGS (where ESPHome puts its defines)
|
||||
for flag in env.get("BUILD_FLAGS", []):
|
||||
if flag == define_flag or flag.startswith(f"{define_flag}="):
|
||||
return True
|
||||
# Also check CPPDEFINES list (parsed defines)
|
||||
for define in env.get("CPPDEFINES", []):
|
||||
if isinstance(define, tuple):
|
||||
if define[0] == name:
|
||||
return True
|
||||
elif define == name:
|
||||
return True
|
||||
return False
|
||||
|
||||
# USE_ESP8266_WAVEFORM_STUBS is defined when no component needs waveform
|
||||
if has_define_flag(env, "USE_ESP8266_WAVEFORM_STUBS"):
|
||||
|
||||
def filter_waveform_from_core(env, node):
|
||||
"""Filter callback to exclude waveform files from framework build."""
|
||||
path = node.get_path()
|
||||
filename = os.path.basename(path)
|
||||
if filename in (
|
||||
"core_esp8266_waveform_pwm.cpp",
|
||||
"core_esp8266_waveform_phase.cpp",
|
||||
):
|
||||
print(f"ESPHome: Excluding {filename} from build (waveform not required)")
|
||||
return None
|
||||
return node
|
||||
|
||||
# Apply the filter to framework sources
|
||||
env.AddBuildMiddleware(filter_waveform_from_core, "**/cores/esp8266/*.cpp")
|
||||
@@ -98,10 +98,8 @@ void ESP8266GPIOPin::pin_mode(gpio::Flags flags) {
|
||||
pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT
|
||||
}
|
||||
|
||||
std::string ESP8266GPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "GPIO%u", pin_);
|
||||
return buffer;
|
||||
size_t ESP8266GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "GPIO%u", this->pin_);
|
||||
}
|
||||
|
||||
bool ESP8266GPIOPin::digital_read() {
|
||||
|
||||
@@ -17,7 +17,7 @@ class ESP8266GPIOPin : public InternalGPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return pin_; }
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifdef USE_ESP8266_WAVEFORM_STUBS
|
||||
|
||||
// Stub implementations for Arduino waveform/PWM functions.
|
||||
//
|
||||
// When the waveform generator is not needed (no esp8266_pwm component),
|
||||
// we exclude core_esp8266_waveform_pwm.cpp from the build to save ~596 bytes
|
||||
// of RAM and 464 bytes of flash.
|
||||
//
|
||||
// These stubs satisfy calls from the Arduino GPIO code when the real
|
||||
// waveform implementation is excluded. They must be in the global namespace
|
||||
// with C linkage to match the Arduino core function declarations.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Empty namespace to satisfy linter - actual stubs must be at global scope
|
||||
namespace esphome::esp8266 {} // namespace esphome::esp8266
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Called by Arduino GPIO code to stop any waveform on a pin
|
||||
int stopWaveform(uint8_t pin) {
|
||||
(void) pin;
|
||||
return 1; // Success (no waveform to stop)
|
||||
}
|
||||
|
||||
// Called by Arduino GPIO code to stop any PWM on a pin
|
||||
bool _stopPWM(uint8_t pin) {
|
||||
(void) pin;
|
||||
return false; // No PWM was running
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // USE_ESP8266_WAVEFORM_STUBS
|
||||
@@ -1,6 +1,7 @@
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
from esphome.components.esp8266.const import require_waveform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_NUMBER, CONF_PIN
|
||||
|
||||
@@ -34,7 +35,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config) -> None:
|
||||
require_waveform()
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await output.register_output(var, config)
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
CONF_SPEED,
|
||||
DEVICE_CLASS_SPEED,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_MEASUREMENT_ANGLE,
|
||||
UNIT_DEGREES,
|
||||
UNIT_KILOMETER_PER_HOUR,
|
||||
UNIT_METER,
|
||||
@@ -21,6 +22,7 @@ CONF_HDOP = "hdop"
|
||||
|
||||
ICON_ALTIMETER = "mdi:altimeter"
|
||||
ICON_COMPASS = "mdi:compass"
|
||||
ICON_CIRCLE_DOUBLE = "mdi:circle-double"
|
||||
ICON_LATITUDE = "mdi:latitude"
|
||||
ICON_LONGITUDE = "mdi:longitude"
|
||||
ICON_SATELLITE = "mdi:satellite-variant"
|
||||
@@ -50,7 +52,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
unit_of_measurement=UNIT_DEGREES,
|
||||
icon=ICON_LONGITUDE,
|
||||
accuracy_decimals=6,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT_ANGLE,
|
||||
),
|
||||
cv.Optional(CONF_SPEED): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOMETER_PER_HOUR,
|
||||
@@ -63,7 +65,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
unit_of_measurement=UNIT_DEGREES,
|
||||
icon=ICON_COMPASS,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT_ANGLE,
|
||||
),
|
||||
cv.Optional(CONF_ALTITUDE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
@@ -72,11 +74,14 @@ CONFIG_SCHEMA = cv.All(
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_SATELLITES): sensor.sensor_schema(
|
||||
# no unit_of_measurement
|
||||
icon=ICON_SATELLITE,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HDOP): sensor.sensor_schema(
|
||||
# no unit_of_measurement
|
||||
icon=ICON_CIRCLE_DOUBLE,
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
|
||||
@@ -34,9 +34,9 @@ void HLW8012Component::setup() {
|
||||
}
|
||||
void HLW8012Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "HLW8012:");
|
||||
LOG_PIN(" SEL Pin: ", this->sel_pin_)
|
||||
LOG_PIN(" CF Pin: ", this->cf_pin_)
|
||||
LOG_PIN(" CF1 Pin: ", this->cf1_pin_)
|
||||
LOG_PIN(" SEL Pin: ", this->sel_pin_);
|
||||
LOG_PIN(" CF Pin: ", this->cf_pin_);
|
||||
LOG_PIN(" CF1 Pin: ", this->cf1_pin_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Change measurement mode every %" PRIu32 "\n"
|
||||
" Current resistor: %.1f mΩ\n"
|
||||
|
||||
@@ -25,11 +25,7 @@ void HostGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::Interr
|
||||
}
|
||||
void HostGPIOPin::pin_mode(gpio::Flags flags) { ESP_LOGD(TAG, "Setting pin %d mode to %02X", pin_, (uint32_t) flags); }
|
||||
|
||||
std::string HostGPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "GPIO%u", pin_);
|
||||
return buffer;
|
||||
}
|
||||
size_t HostGPIOPin::dump_summary(char *buffer, size_t len) const { return snprintf(buffer, len, "GPIO%u", this->pin_); }
|
||||
|
||||
bool HostGPIOPin::digital_read() { return inverted_; }
|
||||
void HostGPIOPin::digital_write(bool value) {
|
||||
|
||||
@@ -17,7 +17,7 @@ class HostGPIOPin : public InternalGPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return pin_; }
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace hte501 {
|
||||
|
||||
static const char *const TAG = "hte501";
|
||||
|
||||
static constexpr size_t HTE501_SERIAL_NUMBER_SIZE = 7;
|
||||
|
||||
void HTE501Component::setup() {
|
||||
uint8_t address[] = {0x70, 0x29};
|
||||
uint8_t identification[9];
|
||||
@@ -16,7 +18,10 @@ void HTE501Component::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(identification + 0, 7).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char serial_hex[format_hex_size(HTE501_SERIAL_NUMBER_SIZE)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex_to(serial_hex, identification, HTE501_SERIAL_NUMBER_SIZE));
|
||||
}
|
||||
|
||||
void HTE501Component::dump_config() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "kuntze.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
@@ -10,11 +11,17 @@ static const char *const TAG = "kuntze";
|
||||
static const uint8_t CMD_READ_REG = 0x03;
|
||||
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
|
||||
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
|
||||
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
|
||||
|
||||
void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); };
|
||||
|
||||
this->waiting_ = false;
|
||||
ESP_LOGV(TAG, "Data: %s", format_hex_pretty(data).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(KUNTZE_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Data: %s", format_hex_pretty_to(hex_buf, data.data(), data.size()));
|
||||
|
||||
float value = (float) get_16bit(0);
|
||||
for (int i = 0; i < data[3]; i++)
|
||||
|
||||
@@ -63,10 +63,8 @@ void ArduinoInternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
pinMode(pin_, flags_to_mode(flags)); // NOLINT
|
||||
}
|
||||
|
||||
std::string ArduinoInternalGPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%u", pin_);
|
||||
return buffer;
|
||||
size_t ArduinoInternalGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u", this->pin_);
|
||||
}
|
||||
|
||||
bool ArduinoInternalGPIOPin::digital_read() {
|
||||
|
||||
@@ -16,7 +16,7 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return pin_; }
|
||||
|
||||
@@ -161,10 +161,8 @@ void MAX6956GPIOPin::setup() { pin_mode(flags_); }
|
||||
void MAX6956GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||
bool MAX6956GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
|
||||
void MAX6956GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
|
||||
std::string MAX6956GPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%u via Max6956", pin_);
|
||||
return buffer;
|
||||
size_t MAX6956GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u via Max6956", this->pin_);
|
||||
}
|
||||
|
||||
} // namespace max6956
|
||||
|
||||
@@ -76,7 +76,7 @@ class MAX6956GPIOPin : public GPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_parent(MAX6956 *parent) { parent_ = parent; }
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
|
||||
@@ -99,10 +99,8 @@ void MCP23016GPIOPin::setup() { pin_mode(flags_); }
|
||||
void MCP23016GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||
bool MCP23016GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
|
||||
void MCP23016GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
|
||||
std::string MCP23016GPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%u via MCP23016", pin_);
|
||||
return buffer;
|
||||
size_t MCP23016GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u via MCP23016", this->pin_);
|
||||
}
|
||||
|
||||
} // namespace mcp23016
|
||||
|
||||
@@ -60,7 +60,7 @@ class MCP23016GPIOPin : public GPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_parent(MCP23016 *parent) { parent_ = parent; }
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
|
||||
@@ -16,8 +16,8 @@ template<uint8_t N> bool MCP23XXXGPIOPin<N>::digital_read() {
|
||||
template<uint8_t N> void MCP23XXXGPIOPin<N>::digital_write(bool value) {
|
||||
this->parent_->digital_write(this->pin_, value != this->inverted_);
|
||||
}
|
||||
template<uint8_t N> std::string MCP23XXXGPIOPin<N>::dump_summary() const {
|
||||
return str_snprintf("%u via MCP23XXX", 15, pin_);
|
||||
template<uint8_t N> size_t MCP23XXXGPIOPin<N>::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u via MCP23XXX", this->pin_);
|
||||
}
|
||||
|
||||
template class MCP23XXXGPIOPin<8>;
|
||||
|
||||
@@ -36,7 +36,7 @@ template<uint8_t N> class MCP23XXXGPIOPin : public GPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_parent(MCP23XXXBase<N> *parent) { parent_ = parent; }
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
|
||||
@@ -153,10 +153,8 @@ void MPR121GPIOPin::digital_write(bool value) {
|
||||
this->parent_->digital_write(this->pin_ - 4, value != this->inverted_);
|
||||
}
|
||||
|
||||
std::string MPR121GPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "ELE%u on MPR121", this->pin_);
|
||||
return buffer;
|
||||
size_t MPR121GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "ELE%u on MPR121", this->pin_);
|
||||
}
|
||||
|
||||
} // namespace mpr121
|
||||
|
||||
@@ -109,7 +109,7 @@ class MPR121GPIOPin : public GPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_parent(MPR121Component *parent) { this->parent_ = parent; }
|
||||
void set_pin(uint8_t pin) { this->pin_ = pin; }
|
||||
|
||||
@@ -180,10 +180,8 @@ void PCA6416AGPIOPin::setup() { pin_mode(flags_); }
|
||||
void PCA6416AGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||
bool PCA6416AGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
|
||||
void PCA6416AGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
|
||||
std::string PCA6416AGPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%u via PCA6416A", pin_);
|
||||
return buffer;
|
||||
size_t PCA6416AGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u via PCA6416A", this->pin_);
|
||||
}
|
||||
|
||||
} // namespace pca6416a
|
||||
|
||||
@@ -52,7 +52,7 @@ class PCA6416AGPIOPin : public GPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_parent(PCA6416AComponent *parent) { parent_ = parent; }
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
|
||||
@@ -129,10 +129,8 @@ void PCA9554GPIOPin::setup() { pin_mode(flags_); }
|
||||
void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||
bool PCA9554GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
|
||||
void PCA9554GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
|
||||
std::string PCA9554GPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%u via PCA9554", pin_);
|
||||
return buffer;
|
||||
size_t PCA9554GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u via PCA9554", this->pin_);
|
||||
}
|
||||
|
||||
} // namespace pca9554
|
||||
|
||||
@@ -59,7 +59,7 @@ class PCA9554GPIOPin : public GPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_parent(PCA9554Component *parent) { parent_ = parent; }
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
|
||||
@@ -104,10 +104,8 @@ void PCF8574GPIOPin::setup() { pin_mode(flags_); }
|
||||
void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||
bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
|
||||
void PCF8574GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
|
||||
std::string PCF8574GPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%u via PCF8574", pin_);
|
||||
return buffer;
|
||||
size_t PCF8574GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u via PCF8574", this->pin_);
|
||||
}
|
||||
|
||||
} // namespace pcf8574
|
||||
|
||||
@@ -54,7 +54,7 @@ class PCF8574GPIOPin : public GPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_parent(PCF8574Component *parent) { parent_ = parent; }
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
|
||||
@@ -164,7 +164,9 @@ bool PI4IOE5V6408GPIOPin::digital_read() { return this->parent_->digital_read(th
|
||||
void PI4IOE5V6408GPIOPin::digital_write(bool value) {
|
||||
this->parent_->digital_write(this->pin_, value != this->inverted_);
|
||||
}
|
||||
std::string PI4IOE5V6408GPIOPin::dump_summary() const { return str_sprintf("%u via PI4IOE5V6408", this->pin_); }
|
||||
size_t PI4IOE5V6408GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u via PI4IOE5V6408", this->pin_);
|
||||
}
|
||||
|
||||
} // namespace pi4ioe5v6408
|
||||
} // namespace esphome
|
||||
|
||||
@@ -52,7 +52,7 @@ class PI4IOE5V6408GPIOPin : public GPIOPin, public Parented<PI4IOE5V6408Componen
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_pin(uint8_t pin) { this->pin_ = pin; }
|
||||
void set_inverted(bool inverted) { this->inverted_ = inverted; }
|
||||
|
||||
@@ -64,10 +64,8 @@ void RP2040GPIOPin::pin_mode(gpio::Flags flags) {
|
||||
pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT
|
||||
}
|
||||
|
||||
std::string RP2040GPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "GPIO%u", pin_);
|
||||
return buffer;
|
||||
size_t RP2040GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "GPIO%u", this->pin_);
|
||||
}
|
||||
|
||||
bool RP2040GPIOPin::digital_read() {
|
||||
|
||||
@@ -18,7 +18,7 @@ class RP2040GPIOPin : public InternalGPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return pin_; }
|
||||
|
||||
@@ -64,7 +64,9 @@ float SN74HC165Component::get_setup_priority() const { return setup_priority::IO
|
||||
|
||||
bool SN74HC165GPIOPin::digital_read() { return this->parent_->digital_read_(this->pin_) != this->inverted_; }
|
||||
|
||||
std::string SN74HC165GPIOPin::dump_summary() const { return str_snprintf("%u via SN74HC165", 18, pin_); }
|
||||
size_t SN74HC165GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u via SN74HC165", this->pin_);
|
||||
}
|
||||
|
||||
} // namespace sn74hc165
|
||||
} // namespace esphome
|
||||
|
||||
@@ -47,7 +47,7 @@ class SN74HC165GPIOPin : public GPIOPin, public Parented<SN74HC165Component> {
|
||||
void pin_mode(gpio::Flags flags) override {}
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override{};
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_pin(uint16_t pin) { pin_ = pin; }
|
||||
void set_inverted(bool inverted) { inverted_ = inverted; }
|
||||
|
||||
@@ -93,7 +93,9 @@ float SN74HC595Component::get_setup_priority() const { return setup_priority::IO
|
||||
void SN74HC595GPIOPin::digital_write(bool value) {
|
||||
this->parent_->digital_write_(this->pin_, value != this->inverted_);
|
||||
}
|
||||
std::string SN74HC595GPIOPin::dump_summary() const { return str_snprintf("%u via SN74HC595", 18, pin_); }
|
||||
size_t SN74HC595GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u via SN74HC595", this->pin_);
|
||||
}
|
||||
|
||||
} // namespace sn74hc595
|
||||
} // namespace esphome
|
||||
|
||||
@@ -54,7 +54,7 @@ class SN74HC595GPIOPin : public GPIOPin, public Parented<SN74HC595Component> {
|
||||
void pin_mode(gpio::Flags flags) override {}
|
||||
bool digital_read() override { return false; }
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_pin(uint16_t pin) { pin_ = pin; }
|
||||
void set_inverted(bool inverted) { inverted_ = inverted; }
|
||||
|
||||
@@ -64,9 +64,9 @@ void SPIComponent::setup() {
|
||||
|
||||
void SPIComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "SPI bus:");
|
||||
LOG_PIN(" CLK Pin: ", this->clk_pin_)
|
||||
LOG_PIN(" SDI Pin: ", this->sdi_pin_)
|
||||
LOG_PIN(" SDO Pin: ", this->sdo_pin_)
|
||||
LOG_PIN(" CLK Pin: ", this->clk_pin_);
|
||||
LOG_PIN(" SDI Pin: ", this->sdi_pin_);
|
||||
LOG_PIN(" SDO Pin: ", this->sdo_pin_);
|
||||
for (size_t i = 0; i != this->data_pins_.size(); i++) {
|
||||
ESP_LOGCONFIG(TAG, " Data pin %u: GPIO%d", i, this->data_pins_[i]);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,11 @@ class NullPin : public GPIOPin {
|
||||
|
||||
void digital_write(bool value) override {}
|
||||
|
||||
std::string dump_summary() const override { return std::string(); }
|
||||
size_t dump_summary(char *buffer, size_t len) const override {
|
||||
if (len > 0)
|
||||
buffer[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected:
|
||||
static GPIOPin *const NULL_PIN; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -12,10 +12,8 @@ void SX1509GPIOPin::setup() { pin_mode(flags_); }
|
||||
void SX1509GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||
bool SX1509GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
|
||||
void SX1509GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
|
||||
std::string SX1509GPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%u via sx1509", this->pin_);
|
||||
return buffer;
|
||||
size_t SX1509GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u via sx1509", this->pin_);
|
||||
}
|
||||
|
||||
} // namespace sx1509
|
||||
|
||||
@@ -13,7 +13,7 @@ class SX1509GPIOPin : public GPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_parent(SX1509Component *parent) { this->parent_ = parent; }
|
||||
void set_pin(uint8_t pin) { this->pin_ = pin; }
|
||||
|
||||
@@ -138,7 +138,9 @@ void TCA9555GPIOPin::setup() { this->pin_mode(this->flags_); }
|
||||
void TCA9555GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||
bool TCA9555GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
|
||||
void TCA9555GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
|
||||
std::string TCA9555GPIOPin::dump_summary() const { return str_sprintf("%u via TCA9555", this->pin_); }
|
||||
size_t TCA9555GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u via TCA9555", this->pin_);
|
||||
}
|
||||
|
||||
} // namespace tca9555
|
||||
} // namespace esphome
|
||||
|
||||
@@ -48,7 +48,7 @@ class TCA9555GPIOPin : public GPIOPin, public Parented<TCA9555Component> {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_pin(uint8_t pin) { this->pin_ = pin; }
|
||||
void set_inverted(bool inverted) { this->inverted_ = inverted; }
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace tee501 {
|
||||
|
||||
static const char *const TAG = "tee501";
|
||||
|
||||
static constexpr size_t TEE501_SERIAL_NUMBER_SIZE = 7;
|
||||
|
||||
void TEE501Component::setup() {
|
||||
uint8_t address[] = {0x70, 0x29};
|
||||
uint8_t identification[9];
|
||||
@@ -17,7 +19,10 @@ void TEE501Component::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(identification + 0, 7).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char serial_hex[format_hex_size(TEE501_SERIAL_NUMBER_SIZE)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex_to(serial_hex, identification, TEE501_SERIAL_NUMBER_SIZE));
|
||||
}
|
||||
|
||||
void TEE501Component::dump_config() {
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace uponor_smatrix {
|
||||
|
||||
static const char *const TAG = "uponor_smatrix";
|
||||
|
||||
// Maximum bytes to log in verbose hex output
|
||||
static constexpr size_t UPONOR_MAX_LOG_BYTES = 36;
|
||||
|
||||
void UponorSmatrixComponent::setup() {
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_ != nullptr) {
|
||||
@@ -97,8 +100,11 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_size(UPONOR_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Received packet: addr=%08X, data=%s, crc=%04X", device_address,
|
||||
format_hex(&packet[4], packet_len - 6).c_str(), crc);
|
||||
format_hex_to(hex_buf, &packet[4], packet_len - 6), crc);
|
||||
|
||||
// Handle packet
|
||||
size_t data_len = (packet_len - 6) / 3;
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace vbus {
|
||||
|
||||
static const char *const TAG = "vbus";
|
||||
|
||||
// Maximum bytes to log in verbose hex output (16 frames * 4 bytes = 64 bytes typical)
|
||||
static constexpr size_t VBUS_MAX_LOG_BYTES = 64;
|
||||
|
||||
void VBus::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "VBus:");
|
||||
check_uart_settings(9600);
|
||||
@@ -101,8 +104,11 @@ void VBus::loop() {
|
||||
this->buffer_.push_back(this->fbytes_[i]);
|
||||
if (++this->cframe_ < this->frames_)
|
||||
continue;
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_size(VBUS_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "P2 C%04x %04x->%04x: %s", this->command_, this->source_, this->dest_,
|
||||
format_hex(this->buffer_).c_str());
|
||||
format_hex_to(hex_buf, this->buffer_.data(), this->buffer_.size()));
|
||||
for (auto &listener : this->listeners_)
|
||||
listener->on_message(this->command_, this->source_, this->dest_, this->buffer_);
|
||||
this->state_ = 0;
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
CONF_ON_CLIENT_DISCONNECTED,
|
||||
CONF_ON_ERROR,
|
||||
CONF_ON_IDLE,
|
||||
CONF_ON_START,
|
||||
CONF_SPEAKER,
|
||||
)
|
||||
|
||||
@@ -24,7 +25,6 @@ CONF_ON_INTENT_END = "on_intent_end"
|
||||
CONF_ON_INTENT_PROGRESS = "on_intent_progress"
|
||||
CONF_ON_INTENT_START = "on_intent_start"
|
||||
CONF_ON_LISTENING = "on_listening"
|
||||
CONF_ON_START = "on_start"
|
||||
CONF_ON_STT_END = "on_stt_end"
|
||||
CONF_ON_STT_VAD_END = "on_stt_vad_end"
|
||||
CONF_ON_STT_VAD_START = "on_stt_vad_start"
|
||||
|
||||
@@ -177,10 +177,10 @@ uint32_t WaveshareEPaper2P13InV3::idle_timeout_() { return 5000; }
|
||||
void WaveshareEPaper2P13InV3::dump_config() {
|
||||
LOG_DISPLAY("", "Waveshare E-Paper", this)
|
||||
ESP_LOGCONFIG(TAG, " Model: 2.13inV3");
|
||||
LOG_PIN(" CS Pin: ", this->cs_)
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_)
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_)
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_)
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -245,10 +245,8 @@ void WeikaiGPIOPin::setup() {
|
||||
this->pin_mode(this->flags_);
|
||||
}
|
||||
|
||||
std::string WeikaiGPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%u via WeiKai %s", this->pin_, this->parent_->get_name());
|
||||
return buffer;
|
||||
size_t WeikaiGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u via WeiKai %s", this->pin_, this->parent_->get_name());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -278,7 +278,7 @@ class WeikaiGPIOPin : public GPIOPin {
|
||||
gpio::Flags get_flags() const override { return this->flags_; }
|
||||
|
||||
void setup() override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void pin_mode(gpio::Flags flags) override { this->parent_->set_pin_direction_(this->pin_, flags); }
|
||||
bool digital_read() override { return this->parent_->read_pin_val_(this->pin_) != this->inverted_; }
|
||||
void digital_write(bool value) override { this->parent_->write_pin_val_(this->pin_, value != this->inverted_); }
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace xiaomi_ble {
|
||||
|
||||
static const char *const TAG = "xiaomi_ble";
|
||||
|
||||
// Maximum bytes to log in very verbose hex output (covers largest packet of ~24 bytes)
|
||||
static constexpr size_t XIAOMI_MAX_LOG_BYTES = 32;
|
||||
|
||||
bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) {
|
||||
// button pressed, 3 bytes, only byte 3 is used for supported devices so far
|
||||
if ((value_type == 0x1001) && (value_length == 3)) {
|
||||
@@ -263,7 +266,10 @@ optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::Service
|
||||
bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, const uint64_t &address) {
|
||||
if ((raw.size() != 19) && ((raw.size() < 22) || (raw.size() > 24))) {
|
||||
ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): data packet has wrong size (%d)!", raw.size());
|
||||
ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(XIAOMI_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty_to(hex_buf, raw.data(), raw.size()));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -320,12 +326,17 @@ bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, c
|
||||
memcpy(mac_address + 4, mac_reverse + 1, 1);
|
||||
memcpy(mac_address + 5, mac_reverse, 1);
|
||||
ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed.");
|
||||
ESP_LOGVV(TAG, " MAC address : %s", format_mac_address_pretty(mac_address).c_str());
|
||||
ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str());
|
||||
ESP_LOGVV(TAG, " Key : %s", format_hex_pretty(vector.key, vector.keysize).c_str());
|
||||
ESP_LOGVV(TAG, " Iv : %s", format_hex_pretty(vector.iv, vector.ivsize).c_str());
|
||||
ESP_LOGVV(TAG, " Cipher : %s", format_hex_pretty(vector.ciphertext, vector.datasize).c_str());
|
||||
ESP_LOGVV(TAG, " Tag : %s", format_hex_pretty(vector.tag, vector.tagsize).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(mac_address, mac_buf);
|
||||
char hex_buf[format_hex_pretty_size(XIAOMI_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, " MAC address : %s", mac_buf);
|
||||
ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty_to(hex_buf, raw.data(), raw.size()));
|
||||
ESP_LOGVV(TAG, " Key : %s", format_hex_pretty_to(hex_buf, vector.key, vector.keysize));
|
||||
ESP_LOGVV(TAG, " Iv : %s", format_hex_pretty_to(hex_buf, vector.iv, vector.ivsize));
|
||||
ESP_LOGVV(TAG, " Cipher : %s", format_hex_pretty_to(hex_buf, vector.ciphertext, vector.datasize));
|
||||
ESP_LOGVV(TAG, " Tag : %s", format_hex_pretty_to(hex_buf, vector.tag, vector.tagsize));
|
||||
mbedtls_ccm_free(&ctx);
|
||||
return false;
|
||||
}
|
||||
@@ -341,8 +352,11 @@ bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, c
|
||||
raw[0] &= ~0x08;
|
||||
|
||||
ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption passed.");
|
||||
ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d", format_hex_pretty(raw.data() + cipher_pos, vector.datasize).c_str(),
|
||||
static_cast<int>(raw[4]));
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(XIAOMI_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d",
|
||||
format_hex_pretty_to(hex_buf, raw.data() + cipher_pos, vector.datasize), static_cast<int>(raw[4]));
|
||||
|
||||
mbedtls_ccm_free(&ctx);
|
||||
return true;
|
||||
|
||||
@@ -110,7 +110,9 @@ void XL9535Component::pin_mode(uint8_t pin, gpio::Flags mode) {
|
||||
|
||||
void XL9535GPIOPin::setup() { this->pin_mode(this->flags_); }
|
||||
|
||||
std::string XL9535GPIOPin::dump_summary() const { return str_snprintf("%u via XL9535", 15, this->pin_); }
|
||||
size_t XL9535GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "%u via XL9535", this->pin_);
|
||||
}
|
||||
|
||||
void XL9535GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||
bool XL9535GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
|
||||
|
||||
@@ -39,7 +39,7 @@ class XL9535GPIOPin : public GPIOPin {
|
||||
gpio::Flags get_flags() const override { return this->flags_; }
|
||||
|
||||
void setup() override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
|
||||
@@ -85,10 +85,8 @@ void ZephyrGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string ZephyrGPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "GPIO%u, P%u.%u", this->pin_, this->pin_ / 32, this->pin_ % 32);
|
||||
return buffer;
|
||||
size_t ZephyrGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "GPIO%u, P%u.%u", this->pin_, this->pin_ / 32, this->pin_ % 32);
|
||||
}
|
||||
|
||||
bool ZephyrGPIOPin::digital_read() {
|
||||
|
||||
@@ -16,7 +16,7 @@ class ZephyrGPIOPin : public InternalGPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return this->pin_; }
|
||||
|
||||
@@ -710,6 +710,7 @@ CONF_ON_RELEASE = "on_release"
|
||||
CONF_ON_RESPONSE = "on_response"
|
||||
CONF_ON_SHUTDOWN = "on_shutdown"
|
||||
CONF_ON_SPEED_SET = "on_speed_set"
|
||||
CONF_ON_START = "on_start"
|
||||
CONF_ON_STATE = "on_state"
|
||||
CONF_ON_SUCCESS = "on_success"
|
||||
CONF_ON_TAG = "on_tag"
|
||||
|
||||
@@ -9,7 +9,8 @@ static const char *const TAG = "entity_base";
|
||||
|
||||
// Entity Name
|
||||
const StringRef &EntityBase::get_name() const { return this->name_; }
|
||||
void EntityBase::set_name(const char *name) {
|
||||
void EntityBase::set_name(const char *name) { this->set_name(name, 0); }
|
||||
void EntityBase::set_name(const char *name, uint32_t object_id_hash) {
|
||||
this->name_ = StringRef(name);
|
||||
if (this->name_.empty()) {
|
||||
#ifdef USE_DEVICES
|
||||
@@ -18,11 +19,29 @@ void EntityBase::set_name(const char *name) {
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->name_ = StringRef(App.get_friendly_name());
|
||||
// Bug-for-bug compatibility with OLD behavior:
|
||||
// - With MAC suffix: OLD code used App.get_friendly_name() directly (no fallback)
|
||||
// - Without MAC suffix: OLD code used pre-computed object_id with fallback to device name
|
||||
const std::string &friendly = App.get_friendly_name();
|
||||
if (App.is_name_add_mac_suffix_enabled()) {
|
||||
// MAC suffix enabled - use friendly_name directly (even if empty) for compatibility
|
||||
this->name_ = StringRef(friendly);
|
||||
} else {
|
||||
// No MAC suffix - fallback to device name if friendly_name is empty
|
||||
this->name_ = StringRef(!friendly.empty() ? friendly : App.get_name());
|
||||
}
|
||||
}
|
||||
this->flags_.has_own_name = false;
|
||||
// Dynamic name - must calculate hash at runtime
|
||||
this->calc_object_id_();
|
||||
} else {
|
||||
this->flags_.has_own_name = true;
|
||||
// Static name - use pre-computed hash if provided
|
||||
if (object_id_hash != 0) {
|
||||
this->object_id_hash_ = object_id_hash;
|
||||
} else {
|
||||
this->calc_object_id_();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,69 +64,30 @@ void EntityBase::set_icon(const char *icon) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Check if the object_id is dynamic (changes with MAC suffix)
|
||||
bool EntityBase::is_object_id_dynamic_() const {
|
||||
return !this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled();
|
||||
}
|
||||
|
||||
// Entity Object ID
|
||||
// Entity Object ID - computed on-demand from name
|
||||
std::string EntityBase::get_object_id() const {
|
||||
// Check if `App.get_friendly_name()` is constant or dynamic.
|
||||
if (this->is_object_id_dynamic_()) {
|
||||
// `App.get_friendly_name()` is dynamic.
|
||||
return str_sanitize(str_snake_case(App.get_friendly_name()));
|
||||
}
|
||||
// `App.get_friendly_name()` is constant.
|
||||
return this->object_id_c_str_ == nullptr ? "" : this->object_id_c_str_;
|
||||
}
|
||||
void EntityBase::set_object_id(const char *object_id) {
|
||||
this->object_id_c_str_ = object_id;
|
||||
this->calc_object_id_();
|
||||
}
|
||||
|
||||
void EntityBase::set_name_and_object_id(const char *name, const char *object_id) {
|
||||
this->set_name(name);
|
||||
this->object_id_c_str_ = object_id;
|
||||
this->calc_object_id_();
|
||||
}
|
||||
|
||||
// Calculate Object ID Hash from Entity Name
|
||||
void EntityBase::calc_object_id_() {
|
||||
char buf[OBJECT_ID_MAX_LEN];
|
||||
StringRef object_id = this->get_object_id_to(buf);
|
||||
this->object_id_hash_ = fnv1_hash(object_id.c_str());
|
||||
size_t len = this->write_object_id_to(buf, sizeof(buf));
|
||||
return std::string(buf, len);
|
||||
}
|
||||
|
||||
// Format dynamic object_id: sanitized snake_case of friendly_name
|
||||
static size_t format_dynamic_object_id(char *buf, size_t buf_size) {
|
||||
const std::string &name = App.get_friendly_name();
|
||||
size_t len = std::min(name.size(), buf_size - 1);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
buf[i] = to_sanitized_char(to_snake_case_char(name[i]));
|
||||
}
|
||||
buf[len] = '\0';
|
||||
return len;
|
||||
// Calculate Object ID Hash directly from name using snake_case + sanitize
|
||||
void EntityBase::calc_object_id_() {
|
||||
this->object_id_hash_ = fnv1_hash_object_id(this->name_.c_str(), this->name_.size());
|
||||
}
|
||||
|
||||
size_t EntityBase::write_object_id_to(char *buf, size_t buf_size) const {
|
||||
if (this->is_object_id_dynamic_()) {
|
||||
return format_dynamic_object_id(buf, buf_size);
|
||||
size_t len = std::min(this->name_.size(), buf_size - 1);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
buf[i] = to_sanitized_char(to_snake_case_char(this->name_[i]));
|
||||
}
|
||||
const char *src = this->object_id_c_str_ == nullptr ? "" : this->object_id_c_str_;
|
||||
size_t len = strlen(src);
|
||||
if (len >= buf_size)
|
||||
len = buf_size - 1;
|
||||
memcpy(buf, src, len);
|
||||
buf[len] = '\0';
|
||||
return len;
|
||||
}
|
||||
|
||||
StringRef EntityBase::get_object_id_to(std::span<char, OBJECT_ID_MAX_LEN> buf) const {
|
||||
if (this->is_object_id_dynamic_()) {
|
||||
size_t len = format_dynamic_object_id(buf.data(), buf.size());
|
||||
return StringRef(buf.data(), len);
|
||||
}
|
||||
return this->object_id_c_str_ == nullptr ? StringRef() : StringRef(this->object_id_c_str_);
|
||||
size_t len = this->write_object_id_to(buf.data(), buf.size());
|
||||
return StringRef(buf.data(), len);
|
||||
}
|
||||
|
||||
uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; }
|
||||
|
||||
@@ -28,6 +28,9 @@ class EntityBase {
|
||||
// Get/set the name of this Entity
|
||||
const StringRef &get_name() const;
|
||||
void set_name(const char *name);
|
||||
/// Set name with pre-computed object_id hash (avoids runtime hash calculation)
|
||||
/// Use hash=0 for dynamic names that need runtime calculation
|
||||
void set_name(const char *name, uint32_t object_id_hash);
|
||||
|
||||
// Get whether this Entity has its own name or it should use the device friendly_name.
|
||||
bool has_own_name() const { return this->flags_.has_own_name; }
|
||||
@@ -43,10 +46,6 @@ class EntityBase {
|
||||
"which will remain available longer. get_object_id() will be removed in 2026.7.0",
|
||||
"2025.12.0")
|
||||
std::string get_object_id() const;
|
||||
void set_object_id(const char *object_id);
|
||||
|
||||
// Set both name and object_id in one call (reduces generated code size)
|
||||
void set_name_and_object_id(const char *name, const char *object_id);
|
||||
|
||||
// Get the unique Object ID of this Entity
|
||||
uint32_t get_object_id_hash();
|
||||
@@ -142,11 +141,7 @@ class EntityBase {
|
||||
protected:
|
||||
void calc_object_id_();
|
||||
|
||||
/// Check if the object_id is dynamic (changes with MAC suffix)
|
||||
bool is_object_id_dynamic_() const;
|
||||
|
||||
StringRef name_;
|
||||
const char *object_id_c_str_{nullptr};
|
||||
#ifdef USE_ENTITY_ICON
|
||||
const char *icon_c_str_{nullptr};
|
||||
#endif
|
||||
|
||||
@@ -15,7 +15,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import MockObj, add, get_variable
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import sanitize, snake_case
|
||||
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
|
||||
from esphome.types import ConfigType, EntityMetadata
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -75,34 +75,18 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
|
||||
config: Configuration dictionary containing entity settings
|
||||
platform: The platform name (e.g., "sensor", "binary_sensor")
|
||||
"""
|
||||
# Get device info
|
||||
device_name: str | None = None
|
||||
device_id_obj: ID | None
|
||||
# Get device info if configured
|
||||
if device_id_obj := config.get(CONF_DEVICE_ID):
|
||||
device: MockObj = await get_variable(device_id_obj)
|
||||
add(var.set_device(device))
|
||||
# Get device name for object ID calculation
|
||||
device_name = device_id_obj.id
|
||||
|
||||
# Calculate base object_id using the same logic as C++
|
||||
# This must match the C++ behavior in esphome/core/entity_base.cpp
|
||||
base_object_id = get_base_entity_object_id(
|
||||
config[CONF_NAME], CORE.friendly_name, device_name
|
||||
)
|
||||
|
||||
if not config[CONF_NAME]:
|
||||
_LOGGER.debug(
|
||||
"Entity has empty name, using '%s' as object_id base", base_object_id
|
||||
)
|
||||
|
||||
# Set both name and object_id in one call to reduce generated code size
|
||||
add(var.set_name_and_object_id(config[CONF_NAME], base_object_id))
|
||||
_LOGGER.debug(
|
||||
"Setting object_id '%s' for entity '%s' on platform '%s'",
|
||||
base_object_id,
|
||||
config[CONF_NAME],
|
||||
platform,
|
||||
)
|
||||
# Set the entity name with pre-computed object_id hash
|
||||
# For named entities: pre-compute hash from entity name
|
||||
# For empty-name entities: pass 0, C++ calculates hash at runtime from
|
||||
# device name, friendly_name, or app name (bug-for-bug compatibility)
|
||||
entity_name = config[CONF_NAME]
|
||||
object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0
|
||||
add(var.set_name(entity_name, object_id_hash))
|
||||
# Only set disabled_by_default if True (default is False)
|
||||
if config[CONF_DISABLED_BY_DEFAULT]:
|
||||
add(var.set_disabled_by_default(True))
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
void log_pin(const char *tag, const __FlashStringHelper *prefix, GPIOPin *pin) {
|
||||
if (pin == nullptr)
|
||||
return;
|
||||
static constexpr size_t LOG_PIN_PREFIX_MAX_LEN = 32;
|
||||
char prefix_buf[LOG_PIN_PREFIX_MAX_LEN];
|
||||
strncpy_P(prefix_buf, reinterpret_cast<const char *>(prefix), sizeof(prefix_buf) - 1);
|
||||
prefix_buf[sizeof(prefix_buf) - 1] = '\0';
|
||||
log_pin_with_prefix(tag, prefix_buf, pin);
|
||||
}
|
||||
#else
|
||||
void log_pin(const char *tag, const char *prefix, GPIOPin *pin) {
|
||||
if (pin == nullptr)
|
||||
return;
|
||||
log_pin_with_prefix(tag, prefix, pin);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome
|
||||
+61
-5
@@ -1,13 +1,22 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
#define LOG_PIN(prefix, pin) \
|
||||
if ((pin) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, prefix "%s", (pin)->dump_summary().c_str()); \
|
||||
}
|
||||
/// Maximum buffer size for dump_summary output
|
||||
inline constexpr size_t GPIO_SUMMARY_MAX_LEN = 48;
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#define LOG_PIN(prefix, pin) log_pin(TAG, F(prefix), pin)
|
||||
#else
|
||||
#define LOG_PIN(prefix, pin) log_pin(TAG, prefix, pin)
|
||||
#endif
|
||||
|
||||
// put GPIO flags in a namespace to not pollute esphome namespace
|
||||
namespace gpio {
|
||||
@@ -64,7 +73,17 @@ class GPIOPin {
|
||||
|
||||
virtual void digital_write(bool value) = 0;
|
||||
|
||||
virtual std::string dump_summary() const = 0;
|
||||
/// Write a summary of this pin to the provided buffer.
|
||||
/// @param buffer The buffer to write to
|
||||
/// @param len The size of the buffer (must be > 0)
|
||||
/// @return The number of characters that would be written (excluding null terminator),
|
||||
/// which may exceed len-1 if truncation occurred (snprintf semantics)
|
||||
virtual size_t dump_summary(char *buffer, size_t len) const;
|
||||
|
||||
/// Get a summary of this pin as a string.
|
||||
/// @deprecated Use dump_summary(char*, size_t) instead. Will be removed in 2026.7.0.
|
||||
ESPDEPRECATED("Override dump_summary(char*, size_t) instead. Will be removed in 2026.7.0.", "2026.1.0")
|
||||
virtual std::string dump_summary() const;
|
||||
|
||||
virtual bool is_internal() { return false; }
|
||||
};
|
||||
@@ -103,4 +122,41 @@ class InternalGPIOPin : public GPIOPin {
|
||||
virtual void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const = 0;
|
||||
};
|
||||
|
||||
// Inline default implementations for GPIOPin virtual methods.
|
||||
// These provide bridge functionality for backwards compatibility with external components.
|
||||
|
||||
// Default implementation bridges to old std::string method for backwards compatibility.
|
||||
inline size_t GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
if (len == 0)
|
||||
return 0;
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
std::string s = this->dump_summary();
|
||||
#pragma GCC diagnostic pop
|
||||
size_t copy_len = std::min(s.size(), len - 1);
|
||||
memcpy(buffer, s.c_str(), copy_len);
|
||||
buffer[copy_len] = '\0';
|
||||
return s.size(); // Return would-be length (snprintf semantics)
|
||||
}
|
||||
|
||||
// Default implementation returns empty string.
|
||||
// External components should override this if they haven't migrated to buffer-based version.
|
||||
// Remove before 2026.7.0
|
||||
inline std::string GPIOPin::dump_summary() const { return {}; }
|
||||
|
||||
// Inline helper for log_pin - allows compiler to inline into log_pin in gpio.cpp
|
||||
inline void log_pin_with_prefix(const char *tag, const char *prefix, GPIOPin *pin) {
|
||||
char buffer[GPIO_SUMMARY_MAX_LEN];
|
||||
size_t len = pin->dump_summary(buffer, sizeof(buffer));
|
||||
len = std::min(len, sizeof(buffer) - 1);
|
||||
esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, "%s%.*s", prefix, (int) len, buffer);
|
||||
}
|
||||
|
||||
// log_pin function declarations - implementation in gpio.cpp
|
||||
#ifdef USE_ESP8266
|
||||
void log_pin(const char *tag, const __FlashStringHelper *prefix, GPIOPin *pin);
|
||||
#else
|
||||
void log_pin(const char *tag, const char *prefix, GPIOPin *pin);
|
||||
#endif
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -529,6 +529,20 @@ constexpr char to_sanitized_char(char c) {
|
||||
/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores.
|
||||
std::string str_sanitize(const std::string &str);
|
||||
|
||||
/// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations.
|
||||
/// This computes object_id hashes directly from names without creating an intermediate buffer.
|
||||
/// IMPORTANT: Must match Python fnv1_hash_object_id() in esphome/helpers.py.
|
||||
/// If you modify this function, update the Python version and tests in both places.
|
||||
inline uint32_t fnv1_hash_object_id(const char *str, size_t len) {
|
||||
uint32_t hash = FNV1_OFFSET_BASIS;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
hash *= FNV1_PRIME;
|
||||
// Apply snake_case (space->underscore, uppercase->lowercase) then sanitize
|
||||
hash ^= static_cast<uint8_t>(to_sanitized_char(to_snake_case_char(str[i])));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator).
|
||||
std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...);
|
||||
|
||||
|
||||
+25
-3
@@ -35,6 +35,10 @@ IS_MACOS = platform.system() == "Darwin"
|
||||
IS_WINDOWS = platform.system() == "Windows"
|
||||
IS_LINUX = platform.system() == "Linux"
|
||||
|
||||
# FNV-1 hash constants (must match C++ in esphome/core/helpers.h)
|
||||
FNV1_OFFSET_BASIS = 2166136261
|
||||
FNV1_PRIME = 16777619
|
||||
|
||||
|
||||
def ensure_unique_string(preferred_string, current_strings):
|
||||
test_string = preferred_string
|
||||
@@ -49,8 +53,17 @@ def ensure_unique_string(preferred_string, current_strings):
|
||||
return test_string
|
||||
|
||||
|
||||
def fnv1_hash(string: str) -> int:
|
||||
"""FNV-1 32-bit hash function (multiply then XOR)."""
|
||||
hash_value = FNV1_OFFSET_BASIS
|
||||
for char in string:
|
||||
hash_value = (hash_value * FNV1_PRIME) & 0xFFFFFFFF
|
||||
hash_value ^= ord(char)
|
||||
return hash_value
|
||||
|
||||
|
||||
def fnv1a_32bit_hash(string: str) -> int:
|
||||
"""FNV-1a 32-bit hash function.
|
||||
"""FNV-1a 32-bit hash function (XOR then multiply).
|
||||
|
||||
Note: This uses 32-bit hash instead of 64-bit for several reasons:
|
||||
1. ESPHome targets 32-bit microcontrollers with limited RAM (often <320KB)
|
||||
@@ -63,13 +76,22 @@ def fnv1a_32bit_hash(string: str) -> int:
|
||||
a handful of area_ids and device_ids (typically <10 areas and <100
|
||||
devices), making collisions virtually impossible.
|
||||
"""
|
||||
hash_value = 2166136261
|
||||
hash_value = FNV1_OFFSET_BASIS
|
||||
for char in string:
|
||||
hash_value ^= ord(char)
|
||||
hash_value = (hash_value * 16777619) & 0xFFFFFFFF
|
||||
hash_value = (hash_value * FNV1_PRIME) & 0xFFFFFFFF
|
||||
return hash_value
|
||||
|
||||
|
||||
def fnv1_hash_object_id(name: str) -> int:
|
||||
"""Compute FNV-1 hash of name with snake_case + sanitize transformations.
|
||||
|
||||
IMPORTANT: Must produce same result as C++ fnv1_hash_object_id() in helpers.h.
|
||||
Used for pre-computing entity object_id hashes at code generation time.
|
||||
"""
|
||||
return fnv1_hash(sanitize(snake_case(name)))
|
||||
|
||||
|
||||
def strip_accents(value: str) -> str:
|
||||
"""Remove accents from a string."""
|
||||
import unicodedata
|
||||
|
||||
+2
-2
@@ -12,10 +12,10 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==5.1.0
|
||||
click==8.1.7
|
||||
esphome-dashboard==20251013.0
|
||||
aioesphomeapi==43.9.1
|
||||
aioesphomeapi==43.10.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.17 # dashboard_import
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.15 # dashboard_import
|
||||
esphome-glyphsets==0.2.0
|
||||
pillow==11.3.0
|
||||
|
||||
@@ -362,12 +362,12 @@ def create_field_type_info(
|
||||
# Traditional fixed array approach with copy (takes priority)
|
||||
return FixedArrayBytesType(field, fixed_size)
|
||||
|
||||
# For SOURCE_CLIENT only messages (decode but no encode), use pointer
|
||||
# For messages that decode (SOURCE_CLIENT or SOURCE_BOTH), use pointer
|
||||
# for zero-copy access to the receive buffer
|
||||
if needs_decode and not needs_encode:
|
||||
if needs_decode:
|
||||
return PointerToBytesBufferType(field, None)
|
||||
|
||||
# For SOURCE_BOTH/SOURCE_SERVER, explicit annotation is still needed
|
||||
# For SOURCE_SERVER (encode only), explicit annotation is still needed
|
||||
if get_field_opt(field, pb.pointer_to_buffer, False):
|
||||
return PointerToBytesBufferType(field, None)
|
||||
|
||||
|
||||
@@ -552,6 +552,8 @@ def convert_path_to_relative(abspath, current):
|
||||
exclude=[
|
||||
"esphome/components/libretiny/generate_components.py",
|
||||
"esphome/components/web_server/__init__.py",
|
||||
# const.py has absolute import in docstring example for external components
|
||||
"esphome/components/esp8266/const.py",
|
||||
],
|
||||
)
|
||||
def lint_relative_py_import(fname: Path, line, col, content):
|
||||
|
||||
@@ -29,7 +29,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main):
|
||||
)
|
||||
|
||||
# Then
|
||||
assert 'bs_1->set_name_and_object_id("test bs1", "test_bs1");' in main_cpp
|
||||
assert 'bs_1->set_name("test bs1",' in main_cpp
|
||||
assert "bs_1->set_pin(" in main_cpp
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ def test_button_sets_mandatory_fields(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/button/test_button.yaml")
|
||||
|
||||
# Then
|
||||
assert 'wol_1->set_name_and_object_id("wol_test_1", "wol_test_1");' in main_cpp
|
||||
assert 'wol_1->set_name("wol_test_1",' in main_cpp
|
||||
assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ def test_text_sets_mandatory_fields(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
assert 'it_1->set_name_and_object_id("test 1 text", "test_1_text");' in main_cpp
|
||||
assert 'it_1->set_name("test 1 text",' in main_cpp
|
||||
|
||||
|
||||
def test_text_config_value_internal_set(generate_main):
|
||||
|
||||
@@ -25,18 +25,9 @@ def test_text_sensor_sets_mandatory_fields(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert (
|
||||
'ts_1->set_name_and_object_id("Template Text Sensor 1", "template_text_sensor_1");'
|
||||
in main_cpp
|
||||
)
|
||||
assert (
|
||||
'ts_2->set_name_and_object_id("Template Text Sensor 2", "template_text_sensor_2");'
|
||||
in main_cpp
|
||||
)
|
||||
assert (
|
||||
'ts_3->set_name_and_object_id("Template Text Sensor 3", "template_text_sensor_3");'
|
||||
in main_cpp
|
||||
)
|
||||
assert 'ts_1->set_name("Template Text Sensor 1",' in main_cpp
|
||||
assert 'ts_2->set_name("Template Text Sensor 2",' in main_cpp
|
||||
assert 'ts_3->set_name("Template Text Sensor 3",' in main_cpp
|
||||
|
||||
|
||||
def test_text_sensor_config_value_internal_set(generate_main):
|
||||
|
||||
@@ -41,7 +41,7 @@ sensor:
|
||||
switch:
|
||||
- platform: micronova
|
||||
stove:
|
||||
name: Stove on/off
|
||||
name: Stove
|
||||
|
||||
text_sensor:
|
||||
- platform: micronova
|
||||
|
||||
@@ -92,7 +92,7 @@ sensor:
|
||||
ch_pump_starts:
|
||||
name: "Boiler Number of starts CH pump"
|
||||
dhw_pump_valve_starts:
|
||||
name: "Boiler Number of starts DHW pump/valve"
|
||||
name: "Boiler Number of starts DHW pump valve"
|
||||
dhw_burner_starts:
|
||||
name: "Boiler Number of starts burner during DHW mode"
|
||||
burner_operation_hours:
|
||||
@@ -139,7 +139,7 @@ binary_sensor:
|
||||
dhw_present:
|
||||
name: "Boiler DHW present"
|
||||
control_type_on_off:
|
||||
name: "Boiler Control type is on/off"
|
||||
name: "Boiler Control type is on-off"
|
||||
cooling_supported:
|
||||
name: "Boiler Cooling supported"
|
||||
dhw_storage_tank:
|
||||
@@ -153,9 +153,9 @@ binary_sensor:
|
||||
max_ch_setpoint_transfer_enabled:
|
||||
name: "Boiler CH maximum setpoint transfer enabled"
|
||||
dhw_setpoint_rw:
|
||||
name: "Boiler DHW setpoint read/write"
|
||||
name: "Boiler DHW setpoint read-write"
|
||||
max_ch_setpoint_rw:
|
||||
name: "Boiler CH maximum setpoint read/write"
|
||||
name: "Boiler CH maximum setpoint read-write"
|
||||
|
||||
switch:
|
||||
- platform: opentherm
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
climate:
|
||||
- platform: zhlt01
|
||||
name: ZH/LT-01 Climate
|
||||
name: ZH-LT-01 Climate
|
||||
transmitter_id: xmitr
|
||||
|
||||
@@ -51,6 +51,9 @@ if platform.system() == "Windows":
|
||||
|
||||
import pty # not available on Windows
|
||||
|
||||
# Register assert rewrite for entity_utils so assertions have proper error messages
|
||||
pytest.register_assert_rewrite("tests.integration.entity_utils")
|
||||
|
||||
|
||||
def _get_platformio_env(cache_dir: Path) -> dict[str, str]:
|
||||
"""Get environment variables for PlatformIO with shared cache."""
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Utilities for computing entity object_id in integration tests.
|
||||
|
||||
This module contains the algorithm that aioesphomeapi will use to compute
|
||||
object_id client-side from API data.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aioesphomeapi import DeviceInfo, EntityInfo
|
||||
|
||||
|
||||
def compute_object_id(name: str) -> str:
|
||||
"""Compute object_id from name using snake_case + sanitize."""
|
||||
return sanitize(snake_case(name))
|
||||
|
||||
|
||||
def infer_name_add_mac_suffix(device_info: DeviceInfo) -> bool:
|
||||
"""Infer name_add_mac_suffix from device name ending with MAC suffix."""
|
||||
mac_suffix = device_info.mac_address.replace(":", "")[-6:].lower()
|
||||
return device_info.name.endswith(f"-{mac_suffix}")
|
||||
|
||||
|
||||
def _get_name_for_object_id(
|
||||
entity: EntityInfo,
|
||||
device_info: DeviceInfo,
|
||||
device_id_to_name: dict[int, str],
|
||||
) -> str:
|
||||
"""Get the name used for object_id computation.
|
||||
|
||||
This is the algorithm that aioesphomeapi will use to determine which
|
||||
name to use for computing object_id client-side from API data.
|
||||
|
||||
Args:
|
||||
entity: The entity to get name for
|
||||
device_info: Device info from the API
|
||||
device_id_to_name: Mapping of device_id to device name for sub-devices
|
||||
|
||||
Returns:
|
||||
The name to use for object_id computation
|
||||
"""
|
||||
if entity.name:
|
||||
# Named entity: use entity name
|
||||
return entity.name
|
||||
if entity.device_id != 0:
|
||||
# Empty name on sub-device: use sub-device name
|
||||
return device_id_to_name[entity.device_id]
|
||||
if infer_name_add_mac_suffix(device_info) or device_info.friendly_name:
|
||||
# Empty name on main device with MAC suffix or friendly_name: use friendly_name
|
||||
# (even if empty - this is bug-for-bug compatibility for MAC suffix case)
|
||||
return device_info.friendly_name
|
||||
# Empty name on main device, no friendly_name: use device name
|
||||
return device_info.name
|
||||
|
||||
|
||||
def compute_entity_object_id(
|
||||
entity: EntityInfo,
|
||||
device_info: DeviceInfo,
|
||||
device_id_to_name: dict[int, str],
|
||||
) -> str:
|
||||
"""Compute expected object_id for an entity.
|
||||
|
||||
Args:
|
||||
entity: The entity to compute object_id for
|
||||
device_info: Device info from the API
|
||||
device_id_to_name: Mapping of device_id to device name for sub-devices
|
||||
|
||||
Returns:
|
||||
The computed object_id string
|
||||
"""
|
||||
name_for_id = _get_name_for_object_id(entity, device_info, device_id_to_name)
|
||||
return compute_object_id(name_for_id)
|
||||
|
||||
|
||||
def compute_entity_hash(
|
||||
entity: EntityInfo,
|
||||
device_info: DeviceInfo,
|
||||
device_id_to_name: dict[int, str],
|
||||
) -> int:
|
||||
"""Compute expected object_id hash for an entity.
|
||||
|
||||
Args:
|
||||
entity: The entity to compute hash for
|
||||
device_info: Device info from the API
|
||||
device_id_to_name: Mapping of device_id to device name for sub-devices
|
||||
|
||||
Returns:
|
||||
The computed FNV-1 hash
|
||||
"""
|
||||
name_for_id = _get_name_for_object_id(entity, device_info, device_id_to_name)
|
||||
return fnv1_hash_object_id(name_for_id)
|
||||
|
||||
|
||||
def verify_entity_object_id(
|
||||
entity: EntityInfo,
|
||||
device_info: DeviceInfo,
|
||||
device_id_to_name: dict[int, str],
|
||||
) -> None:
|
||||
"""Verify an entity's object_id and hash match the expected values.
|
||||
|
||||
Args:
|
||||
entity: The entity to verify
|
||||
device_info: Device info from the API
|
||||
device_id_to_name: Mapping of device_id to device name for sub-devices
|
||||
|
||||
Raises:
|
||||
AssertionError: If object_id or hash doesn't match expected value
|
||||
"""
|
||||
expected_object_id = compute_entity_object_id(
|
||||
entity, device_info, device_id_to_name
|
||||
)
|
||||
assert entity.object_id == expected_object_id, (
|
||||
f"object_id mismatch for entity '{entity.name}': "
|
||||
f"expected '{expected_object_id}', got '{entity.object_id}'"
|
||||
)
|
||||
|
||||
expected_hash = compute_entity_hash(entity, device_info, device_id_to_name)
|
||||
assert entity.key == expected_hash, (
|
||||
f"hash mismatch for entity '{entity.name}': "
|
||||
f"expected {expected_hash:#x}, got {entity.key:#x}"
|
||||
)
|
||||
|
||||
|
||||
def verify_all_entities(
|
||||
entities: list[EntityInfo],
|
||||
device_info: DeviceInfo,
|
||||
) -> None:
|
||||
"""Verify all entities have correct object_id and hash values.
|
||||
|
||||
Args:
|
||||
entities: List of entities to verify
|
||||
device_info: Device info from the API
|
||||
|
||||
Raises:
|
||||
AssertionError: If any entity's object_id or hash doesn't match
|
||||
"""
|
||||
# Build device_id -> name lookup from sub-devices
|
||||
device_id_to_name = {d.device_id: d.name for d in device_info.devices}
|
||||
|
||||
for entity in entities:
|
||||
verify_entity_object_id(entity, device_info, device_id_to_name)
|
||||
@@ -0,0 +1,76 @@
|
||||
esphome:
|
||||
name: fnv1-hash-object-id-test
|
||||
platformio_options:
|
||||
build_flags:
|
||||
- "-DDEBUG"
|
||||
on_boot:
|
||||
- lambda: |-
|
||||
using esphome::fnv1_hash_object_id;
|
||||
|
||||
// Test basic lowercase (hash matches Python fnv1_hash_object_id("foo"))
|
||||
uint32_t hash_foo = fnv1_hash_object_id("foo", 3);
|
||||
if (hash_foo == 0x408f5e13) {
|
||||
ESP_LOGI("FNV1_OID", "foo PASSED");
|
||||
} else {
|
||||
ESP_LOGE("FNV1_OID", "foo FAILED: 0x%08x != 0x408f5e13", hash_foo);
|
||||
}
|
||||
|
||||
// Test uppercase conversion (should match lowercase)
|
||||
uint32_t hash_Foo = fnv1_hash_object_id("Foo", 3);
|
||||
if (hash_Foo == 0x408f5e13) {
|
||||
ESP_LOGI("FNV1_OID", "upper PASSED");
|
||||
} else {
|
||||
ESP_LOGE("FNV1_OID", "upper FAILED: 0x%08x != 0x408f5e13", hash_Foo);
|
||||
}
|
||||
|
||||
// Test space to underscore conversion ("foo bar" -> "foo_bar")
|
||||
uint32_t hash_space = fnv1_hash_object_id("foo bar", 7);
|
||||
if (hash_space == 0x3ae35aa1) {
|
||||
ESP_LOGI("FNV1_OID", "space PASSED");
|
||||
} else {
|
||||
ESP_LOGE("FNV1_OID", "space FAILED: 0x%08x != 0x3ae35aa1", hash_space);
|
||||
}
|
||||
|
||||
// Test underscore preserved ("foo_bar")
|
||||
uint32_t hash_underscore = fnv1_hash_object_id("foo_bar", 7);
|
||||
if (hash_underscore == 0x3ae35aa1) {
|
||||
ESP_LOGI("FNV1_OID", "underscore PASSED");
|
||||
} else {
|
||||
ESP_LOGE("FNV1_OID", "underscore FAILED: 0x%08x != 0x3ae35aa1", hash_underscore);
|
||||
}
|
||||
|
||||
// Test hyphen preserved ("foo-bar")
|
||||
uint32_t hash_hyphen = fnv1_hash_object_id("foo-bar", 7);
|
||||
if (hash_hyphen == 0x438b12e3) {
|
||||
ESP_LOGI("FNV1_OID", "hyphen PASSED");
|
||||
} else {
|
||||
ESP_LOGE("FNV1_OID", "hyphen FAILED: 0x%08x != 0x438b12e3", hash_hyphen);
|
||||
}
|
||||
|
||||
// Test special chars become underscore ("foo!bar" -> "foo_bar")
|
||||
uint32_t hash_special = fnv1_hash_object_id("foo!bar", 7);
|
||||
if (hash_special == 0x3ae35aa1) {
|
||||
ESP_LOGI("FNV1_OID", "special PASSED");
|
||||
} else {
|
||||
ESP_LOGE("FNV1_OID", "special FAILED: 0x%08x != 0x3ae35aa1", hash_special);
|
||||
}
|
||||
|
||||
// Test complex name ("My Sensor Name" -> "my_sensor_name")
|
||||
uint32_t hash_complex = fnv1_hash_object_id("My Sensor Name", 14);
|
||||
if (hash_complex == 0x2760962a) {
|
||||
ESP_LOGI("FNV1_OID", "complex PASSED");
|
||||
} else {
|
||||
ESP_LOGE("FNV1_OID", "complex FAILED: 0x%08x != 0x2760962a", hash_complex);
|
||||
}
|
||||
|
||||
// Test empty string returns FNV1_OFFSET_BASIS
|
||||
uint32_t hash_empty = fnv1_hash_object_id("", 0);
|
||||
if (hash_empty == 0x811c9dc5) {
|
||||
ESP_LOGI("FNV1_OID", "empty PASSED");
|
||||
} else {
|
||||
ESP_LOGE("FNV1_OID", "empty FAILED: 0x%08x != 0x811c9dc5", hash_empty);
|
||||
}
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
@@ -0,0 +1,125 @@
|
||||
esphome:
|
||||
name: object-id-test
|
||||
friendly_name: Test Device
|
||||
# Enable MAC suffix - host MAC is 98:35:69:ab:f6:79, suffix is "abf679"
|
||||
# friendly_name becomes "Test Device abf679"
|
||||
name_add_mac_suffix: true
|
||||
# Sub-devices for testing empty-name entities on devices
|
||||
devices:
|
||||
- id: sub_device_1
|
||||
name: Sub Device One
|
||||
- id: sub_device_2
|
||||
name: Sub Device Two
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
logger:
|
||||
|
||||
sensor:
|
||||
# Test 1: Basic name -> object_id = "temperature_sensor"
|
||||
- platform: template
|
||||
name: "Temperature Sensor"
|
||||
id: sensor_basic
|
||||
lambda: return 42.0;
|
||||
update_interval: 60s
|
||||
|
||||
# Test 2: Uppercase name -> object_id = "uppercase_name"
|
||||
- platform: template
|
||||
name: "UPPERCASE NAME"
|
||||
id: sensor_uppercase
|
||||
lambda: return 43.0;
|
||||
update_interval: 60s
|
||||
|
||||
# Test 3: Special characters -> object_id = "special__chars_"
|
||||
- platform: template
|
||||
name: "Special!@Chars#"
|
||||
id: sensor_special
|
||||
lambda: return 44.0;
|
||||
update_interval: 60s
|
||||
|
||||
# Test 4: Hyphen preserved -> object_id = "temp-sensor"
|
||||
- platform: template
|
||||
name: "Temp-Sensor"
|
||||
id: sensor_hyphen
|
||||
lambda: return 45.0;
|
||||
update_interval: 60s
|
||||
|
||||
# Test 5: Underscore preserved -> object_id = "temp_sensor"
|
||||
- platform: template
|
||||
name: "Temp_Sensor"
|
||||
id: sensor_underscore
|
||||
lambda: return 46.0;
|
||||
update_interval: 60s
|
||||
|
||||
# Test 6: Mixed case with spaces -> object_id = "living_room_temperature"
|
||||
- platform: template
|
||||
name: "Living Room Temperature"
|
||||
id: sensor_mixed
|
||||
lambda: return 47.0;
|
||||
update_interval: 60s
|
||||
|
||||
# Test 7: Empty name - uses friendly_name with MAC suffix
|
||||
# friendly_name = "Test Device abf679" -> object_id = "test_device_abf679"
|
||||
- platform: template
|
||||
name: ""
|
||||
id: sensor_empty_name
|
||||
lambda: return 48.0;
|
||||
update_interval: 60s
|
||||
|
||||
binary_sensor:
|
||||
# Test 8: Different platform same conversion rules
|
||||
- platform: template
|
||||
name: "Door Open"
|
||||
id: binary_door
|
||||
lambda: return true;
|
||||
|
||||
# Test 9: Numbers in name -> object_id = "sensor_123"
|
||||
- platform: template
|
||||
name: "Sensor 123"
|
||||
id: binary_numbers
|
||||
lambda: return false;
|
||||
|
||||
switch:
|
||||
# Test 10: Long name with multiple spaces
|
||||
- platform: template
|
||||
name: "My Very Long Switch Name Here"
|
||||
id: switch_long
|
||||
lambda: return false;
|
||||
turn_on_action:
|
||||
- logger.log: "on"
|
||||
turn_off_action:
|
||||
- logger.log: "off"
|
||||
|
||||
text_sensor:
|
||||
# Test 11: Name starting with number (should work fine)
|
||||
- platform: template
|
||||
name: "123 Start"
|
||||
id: text_num_start
|
||||
lambda: return {"test"};
|
||||
update_interval: 60s
|
||||
|
||||
button:
|
||||
# Test 12: Named entity on sub-device -> object_id from entity name
|
||||
- platform: template
|
||||
name: "Device Button"
|
||||
id: button_on_device
|
||||
device_id: sub_device_1
|
||||
on_press: []
|
||||
|
||||
# Test 13: Empty name on sub-device -> object_id from device name
|
||||
# Device name "Sub Device One" -> object_id = "sub_device_one"
|
||||
- platform: template
|
||||
name: ""
|
||||
id: button_empty_on_device1
|
||||
device_id: sub_device_1
|
||||
on_press: []
|
||||
|
||||
# Test 14: Empty name on different sub-device
|
||||
# Device name "Sub Device Two" -> object_id = "sub_device_two"
|
||||
- platform: template
|
||||
name: ""
|
||||
id: button_empty_on_device2
|
||||
device_id: sub_device_2
|
||||
on_press: []
|
||||
@@ -0,0 +1,27 @@
|
||||
esphome:
|
||||
name: test-device
|
||||
# friendly_name set but NO MAC suffix
|
||||
# Empty-name entity should use friendly_name for object_id
|
||||
friendly_name: My Friendly Device
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
logger:
|
||||
|
||||
sensor:
|
||||
# Empty name entity - should use friendly_name for object_id
|
||||
# friendly_name = "My Friendly Device" -> object_id = "my_friendly_device"
|
||||
- platform: template
|
||||
name: ""
|
||||
id: sensor_empty_name
|
||||
lambda: return 42.0;
|
||||
update_interval: 60s
|
||||
|
||||
# Named entity for comparison
|
||||
- platform: template
|
||||
name: "Temperature"
|
||||
id: sensor_named
|
||||
lambda: return 43.0;
|
||||
update_interval: 60s
|
||||
@@ -0,0 +1,25 @@
|
||||
esphome:
|
||||
name: test-device
|
||||
# No friendly_name set, no MAC suffix
|
||||
# OLD behavior: object_id = device name because Python pre-computed with fallback
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
logger:
|
||||
|
||||
sensor:
|
||||
# Empty name entity - OLD behavior used device name as fallback
|
||||
- platform: template
|
||||
name: ""
|
||||
id: sensor_empty_name
|
||||
lambda: return 42.0;
|
||||
update_interval: 60s
|
||||
|
||||
# Named entity for comparison
|
||||
- platform: template
|
||||
name: "Temperature"
|
||||
id: sensor_named
|
||||
lambda: return 43.0;
|
||||
update_interval: 60s
|
||||
@@ -0,0 +1,26 @@
|
||||
esphome:
|
||||
name: test-device
|
||||
# No friendly_name set, MAC suffix enabled
|
||||
# OLD behavior: object_id = "" (empty) because is_object_id_dynamic_() used App.get_friendly_name() directly
|
||||
name_add_mac_suffix: true
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
logger:
|
||||
|
||||
sensor:
|
||||
# Empty name entity - OLD behavior produced empty object_id when MAC suffix enabled
|
||||
- platform: template
|
||||
name: ""
|
||||
id: sensor_empty_name
|
||||
lambda: return 42.0;
|
||||
update_interval: 60s
|
||||
|
||||
# Named entity for comparison
|
||||
- platform: template
|
||||
name: "Temperature"
|
||||
id: sensor_named
|
||||
lambda: return 43.0;
|
||||
update_interval: 60s
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Integration test for fnv1_hash_object_id function.
|
||||
|
||||
This test verifies that the C++ fnv1_hash_object_id() function in
|
||||
esphome/core/helpers.h produces the same hash values as the Python
|
||||
fnv1_hash_object_id() function in esphome/helpers.py.
|
||||
|
||||
If this test fails, one of the implementations has diverged and needs
|
||||
to be updated to match the other.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fnv1_hash_object_id(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that C++ fnv1_hash_object_id matches Python implementation."""
|
||||
|
||||
test_results: dict[str, str] = {}
|
||||
all_tests_complete = asyncio.Event()
|
||||
expected_tests = {
|
||||
"foo",
|
||||
"upper",
|
||||
"space",
|
||||
"underscore",
|
||||
"hyphen",
|
||||
"special",
|
||||
"complex",
|
||||
"empty",
|
||||
}
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
"""Capture log lines with test results."""
|
||||
# Strip ANSI escape codes
|
||||
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
|
||||
# Look for our test result messages
|
||||
# Format: "[timestamp][level][FNV1_OID:line]: test_name PASSED"
|
||||
match = re.search(r"\[FNV1_OID:\d+\]:\s+(\w+)\s+(PASSED|FAILED)", clean_line)
|
||||
if match:
|
||||
test_name = match.group(1)
|
||||
result = match.group(2)
|
||||
test_results[test_name] = result
|
||||
if set(test_results.keys()) >= expected_tests:
|
||||
all_tests_complete.set()
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=on_log_line),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "fnv1-hash-object-id-test"
|
||||
|
||||
# Wait for all tests to complete or timeout
|
||||
try:
|
||||
await asyncio.wait_for(all_tests_complete.wait(), timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Tests timed out. Got results for: {set(test_results.keys())}")
|
||||
|
||||
# Verify all tests passed
|
||||
for test_name in expected_tests:
|
||||
assert test_name in test_results, f"{test_name} test not found"
|
||||
assert test_results[test_name] == "PASSED", (
|
||||
f"{test_name} test failed - C++ and Python hash mismatch"
|
||||
)
|
||||
@@ -0,0 +1,176 @@
|
||||
"""Integration test to verify object_id from API matches Python computation.
|
||||
|
||||
This test verifies a three-way match between:
|
||||
1. C++ object_id generation (get_object_id_to using to_sanitized_char/to_snake_case_char)
|
||||
2. C++ hash generation (fnv1_hash_object_id in helpers.h)
|
||||
3. Python computation (sanitize/snake_case in helpers.py, fnv1_hash_object_id)
|
||||
|
||||
The API response contains C++ computed values, so verifying API == Python
|
||||
implicitly verifies C++ == Python == API for both object_id and hash.
|
||||
|
||||
This is important for the planned migration to remove object_id from the API
|
||||
protocol and have clients (like aioesphomeapi) compute it from the name.
|
||||
See: https://github.com/esphome/backlog/issues/76
|
||||
|
||||
Test cases covered:
|
||||
- Named entities with various characters (uppercase, special chars, hyphens, etc.)
|
||||
- Empty-name entities on main device (uses device's friendly_name with MAC suffix)
|
||||
- Empty-name entities on sub-devices (uses sub-device's name)
|
||||
- Named entities on sub-devices (uses entity name, not device name)
|
||||
- MAC suffix handling (name_add_mac_suffix modifies friendly_name at runtime)
|
||||
- Both object_id string and hash (key) verification
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.helpers import fnv1_hash_object_id
|
||||
|
||||
from .entity_utils import compute_object_id, verify_all_entities
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679"
|
||||
MAC_SUFFIX = "abf679"
|
||||
|
||||
|
||||
# Expected entities with their own names and expected object_ids
|
||||
# Format: (entity_name, expected_object_id)
|
||||
NAMED_ENTITIES = [
|
||||
# sensor platform
|
||||
("Temperature Sensor", "temperature_sensor"),
|
||||
("UPPERCASE NAME", "uppercase_name"),
|
||||
("Special!@Chars#", "special__chars_"),
|
||||
("Temp-Sensor", "temp-sensor"),
|
||||
("Temp_Sensor", "temp_sensor"),
|
||||
("Living Room Temperature", "living_room_temperature"),
|
||||
# binary_sensor platform
|
||||
("Door Open", "door_open"),
|
||||
("Sensor 123", "sensor_123"),
|
||||
# switch platform
|
||||
("My Very Long Switch Name Here", "my_very_long_switch_name_here"),
|
||||
# text_sensor platform
|
||||
("123 Start", "123_start"),
|
||||
# button platform - named entity on sub-device (uses entity name, not device name)
|
||||
("Device Button", "device_button"),
|
||||
]
|
||||
|
||||
# Sub-device names and their expected object_ids for empty-name entities
|
||||
# Format: (device_name, expected_object_id)
|
||||
SUB_DEVICE_EMPTY_NAME_ENTITIES = [
|
||||
("Sub Device One", "sub_device_one"),
|
||||
("Sub Device Two", "sub_device_two"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_id_api_verification(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that object_id from API matches Python computation.
|
||||
|
||||
Tests:
|
||||
1. Named entities - object_id computed from entity name
|
||||
2. Empty-name entities - object_id computed from friendly_name (with MAC suffix)
|
||||
3. Hash verification - key can be computed from name
|
||||
4. Generic verification - all entities can have object_id computed from API data
|
||||
"""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
# Get device info
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
|
||||
# Device name should include MAC suffix (hyphen separator)
|
||||
assert device_info.name == f"object-id-test-{MAC_SUFFIX}", (
|
||||
f"Device name mismatch: got '{device_info.name}'"
|
||||
)
|
||||
# Friendly name should include MAC suffix (space separator)
|
||||
expected_friendly_name = f"Test Device {MAC_SUFFIX}"
|
||||
assert device_info.friendly_name == expected_friendly_name, (
|
||||
f"Friendly name mismatch: got '{device_info.friendly_name}'"
|
||||
)
|
||||
|
||||
# Get all entities
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Create a map of entity names to entity info
|
||||
entity_map = {}
|
||||
for entity in entities:
|
||||
entity_map[entity.name] = entity
|
||||
|
||||
# === Test 1: Verify each named entity ===
|
||||
for entity_name, expected_object_id in NAMED_ENTITIES:
|
||||
assert entity_name in entity_map, (
|
||||
f"Entity '{entity_name}' not found in API response. "
|
||||
f"Available: {list(entity_map.keys())}"
|
||||
)
|
||||
|
||||
entity = entity_map[entity_name]
|
||||
|
||||
# Verify object_id matches expected
|
||||
assert entity.object_id == expected_object_id, (
|
||||
f"Entity '{entity_name}': object_id mismatch. "
|
||||
f"API returned '{entity.object_id}', expected '{expected_object_id}'"
|
||||
)
|
||||
|
||||
# Verify Python computation matches
|
||||
computed = compute_object_id(entity_name)
|
||||
assert computed == expected_object_id, (
|
||||
f"Entity '{entity_name}': Python computation mismatch. "
|
||||
f"Computed '{computed}', expected '{expected_object_id}'"
|
||||
)
|
||||
|
||||
# Verify hash can be computed from the name
|
||||
hash_from_name = fnv1_hash_object_id(entity_name)
|
||||
assert hash_from_name == entity.key, (
|
||||
f"Entity '{entity_name}': hash mismatch. "
|
||||
f"Python hash {hash_from_name:#x}, API key {entity.key:#x}"
|
||||
)
|
||||
|
||||
# === Test 2: Verify empty-name entities ===
|
||||
# Empty-name entities have name="" in API, object_id comes from:
|
||||
# - Main device: friendly_name (with MAC suffix)
|
||||
# - Sub-device: device name
|
||||
|
||||
# Get all empty-name entities
|
||||
empty_name_entities = [e for e in entities if e.name == ""]
|
||||
# We expect 3: 1 on main device, 2 on sub-devices
|
||||
assert len(empty_name_entities) == 3, (
|
||||
f"Expected 3 empty-name entities, got {len(empty_name_entities)}"
|
||||
)
|
||||
|
||||
# Build device_id -> device_name map from device_info
|
||||
device_id_to_name = {d.device_id: d.name for d in device_info.devices}
|
||||
|
||||
# Verify each empty-name entity
|
||||
for entity in empty_name_entities:
|
||||
if entity.device_id == 0:
|
||||
# Main device - uses friendly_name with MAC suffix
|
||||
expected_name = expected_friendly_name
|
||||
else:
|
||||
# Sub-device - uses device name
|
||||
assert entity.device_id in device_id_to_name, (
|
||||
f"Entity device_id {entity.device_id} not found in devices"
|
||||
)
|
||||
expected_name = device_id_to_name[entity.device_id]
|
||||
|
||||
expected_object_id = compute_object_id(expected_name)
|
||||
assert entity.object_id == expected_object_id, (
|
||||
f"Empty-name entity (device_id={entity.device_id}): object_id mismatch. "
|
||||
f"API: '{entity.object_id}', expected: '{expected_object_id}' "
|
||||
f"(from name '{expected_name}')"
|
||||
)
|
||||
|
||||
# Verify hash matches
|
||||
expected_hash = fnv1_hash_object_id(expected_name)
|
||||
assert entity.key == expected_hash, (
|
||||
f"Empty-name entity (device_id={entity.device_id}): hash mismatch. "
|
||||
f"API key: {entity.key:#x}, expected: {expected_hash:#x}"
|
||||
)
|
||||
|
||||
# === Test 3: Verify ALL entities using the algorithm from entity_utils ===
|
||||
# This uses the algorithm that aioesphomeapi will use to compute object_id
|
||||
# client-side from API data.
|
||||
verify_all_entities(entities, device_info)
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Integration test for object_id with friendly_name but no MAC suffix.
|
||||
|
||||
This test covers Branch 4 of the algorithm:
|
||||
- Empty name on main device
|
||||
- NO MAC suffix enabled
|
||||
- friendly_name IS set
|
||||
- Result: use friendly_name for object_id
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.helpers import fnv1_hash_object_id
|
||||
|
||||
from .entity_utils import (
|
||||
compute_object_id,
|
||||
infer_name_add_mac_suffix,
|
||||
verify_all_entities,
|
||||
)
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_id_friendly_name_no_mac_suffix(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test object_id when friendly_name is set but no MAC suffix.
|
||||
|
||||
This covers Branch 4 of the algorithm:
|
||||
- Empty name entity
|
||||
- name_add_mac_suffix = false (or not set)
|
||||
- friendly_name = "My Friendly Device"
|
||||
- Expected: object_id = "my_friendly_device"
|
||||
"""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
|
||||
# Device name should NOT include MAC suffix
|
||||
assert device_info.name == "test-device"
|
||||
|
||||
# Friendly name should be set
|
||||
assert device_info.friendly_name == "My Friendly Device"
|
||||
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Find the empty-name entity
|
||||
empty_name_entities = [e for e in entities if e.name == ""]
|
||||
assert len(empty_name_entities) == 1
|
||||
|
||||
entity = empty_name_entities[0]
|
||||
|
||||
# Should use friendly_name for object_id (Branch 4)
|
||||
expected_object_id = compute_object_id("My Friendly Device")
|
||||
assert expected_object_id == "my_friendly_device" # Verify our expectation
|
||||
assert entity.object_id == expected_object_id, (
|
||||
f"Expected object_id '{expected_object_id}' from friendly_name, "
|
||||
f"got '{entity.object_id}'"
|
||||
)
|
||||
|
||||
# Hash should match friendly_name
|
||||
expected_hash = fnv1_hash_object_id("My Friendly Device")
|
||||
assert entity.key == expected_hash, (
|
||||
f"Expected hash {expected_hash:#x}, got {entity.key:#x}"
|
||||
)
|
||||
|
||||
# Named entity should work normally
|
||||
named_entities = [e for e in entities if e.name == "Temperature"]
|
||||
assert len(named_entities) == 1
|
||||
assert named_entities[0].object_id == "temperature"
|
||||
|
||||
# Verify our inference: no MAC suffix in this test
|
||||
assert not infer_name_add_mac_suffix(device_info), (
|
||||
"Device name should NOT have MAC suffix"
|
||||
)
|
||||
|
||||
# Verify the full algorithm from entity_utils works for ALL entities
|
||||
verify_all_entities(entities, device_info)
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Integration tests for object_id when friendly_name is not set.
|
||||
|
||||
These tests verify bug-for-bug compatibility with the old behavior:
|
||||
|
||||
1. With MAC suffix enabled + no friendly_name:
|
||||
- OLD: is_object_id_dynamic_() was true, used App.get_friendly_name() directly
|
||||
- OLD: object_id = "" (empty) because friendly_name was empty
|
||||
- NEW: Must maintain same behavior for compatibility
|
||||
|
||||
2. Without MAC suffix + no friendly_name:
|
||||
- OLD: is_object_id_dynamic_() was false, used pre-computed object_id_c_str_
|
||||
- OLD: Python computed object_id with fallback to device name
|
||||
- NEW: Must maintain same behavior (object_id = device name)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.helpers import fnv1_hash_object_id
|
||||
|
||||
from .entity_utils import compute_object_id, verify_all_entities
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679"
|
||||
MAC_SUFFIX = "abf679"
|
||||
|
||||
# FNV1 offset basis - hash of empty string
|
||||
FNV1_OFFSET_BASIS = 2166136261
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_id_no_friendly_name_with_mac_suffix(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test object_id when friendly_name not set but MAC suffix enabled.
|
||||
|
||||
OLD behavior (bug-for-bug compatibility):
|
||||
- is_object_id_dynamic_() returned true (no own name AND mac suffix enabled)
|
||||
- format_dynamic_object_id() used App.get_friendly_name() directly
|
||||
- Since friendly_name was empty, object_id was empty
|
||||
|
||||
This was arguably a bug, but we maintain it for compatibility.
|
||||
"""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
|
||||
# Device name should include MAC suffix
|
||||
expected_device_name = f"test-device-{MAC_SUFFIX}"
|
||||
assert device_info.name == expected_device_name
|
||||
|
||||
# Friendly name should be empty (not set in config)
|
||||
assert device_info.friendly_name == ""
|
||||
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Find the empty-name entity
|
||||
empty_name_entities = [e for e in entities if e.name == ""]
|
||||
assert len(empty_name_entities) == 1
|
||||
|
||||
entity = empty_name_entities[0]
|
||||
|
||||
# OLD behavior: object_id was empty because App.get_friendly_name() was empty
|
||||
# This is bug-for-bug compatibility
|
||||
assert entity.object_id == "", (
|
||||
f"Expected empty object_id for bug-for-bug compatibility, "
|
||||
f"got '{entity.object_id}'"
|
||||
)
|
||||
|
||||
# Hash should be FNV1_OFFSET_BASIS (hash of empty string)
|
||||
assert entity.key == FNV1_OFFSET_BASIS, (
|
||||
f"Expected hash of empty string ({FNV1_OFFSET_BASIS:#x}), "
|
||||
f"got {entity.key:#x}"
|
||||
)
|
||||
|
||||
# Named entity should work normally
|
||||
named_entities = [e for e in entities if e.name == "Temperature"]
|
||||
assert len(named_entities) == 1
|
||||
assert named_entities[0].object_id == "temperature"
|
||||
|
||||
# Verify the full algorithm from entity_utils works for ALL entities
|
||||
verify_all_entities(entities, device_info)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_id_no_friendly_name_no_mac_suffix(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test object_id when friendly_name not set and no MAC suffix.
|
||||
|
||||
OLD behavior:
|
||||
- is_object_id_dynamic_() returned false (mac suffix not enabled)
|
||||
- Used object_id_c_str_ which was pre-computed in Python
|
||||
- Python used get_base_entity_object_id() with fallback to CORE.name
|
||||
|
||||
Result: object_id = sanitize(snake_case(device_name))
|
||||
"""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
|
||||
# Device name should NOT include MAC suffix
|
||||
assert device_info.name == "test-device"
|
||||
|
||||
# Friendly name should be empty (not set in config)
|
||||
assert device_info.friendly_name == ""
|
||||
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Find the empty-name entity
|
||||
empty_name_entities = [e for e in entities if e.name == ""]
|
||||
assert len(empty_name_entities) == 1
|
||||
|
||||
entity = empty_name_entities[0]
|
||||
|
||||
# OLD behavior: object_id was computed from device name
|
||||
expected_object_id = compute_object_id("test-device")
|
||||
assert entity.object_id == expected_object_id, (
|
||||
f"Expected object_id '{expected_object_id}' from device name, "
|
||||
f"got '{entity.object_id}'"
|
||||
)
|
||||
|
||||
# Hash should match device name
|
||||
expected_hash = fnv1_hash_object_id("test-device")
|
||||
assert entity.key == expected_hash, (
|
||||
f"Expected hash {expected_hash:#x}, got {entity.key:#x}"
|
||||
)
|
||||
|
||||
# Named entity should work normally
|
||||
named_entities = [e for e in entities if e.name == "Temperature"]
|
||||
assert len(named_entities) == 1
|
||||
assert named_entities[0].object_id == "temperature"
|
||||
|
||||
# Verify the full algorithm from entity_utils works for ALL entities
|
||||
verify_all_entities(entities, device_info)
|
||||
@@ -27,13 +27,9 @@ from esphome.helpers import sanitize, snake_case
|
||||
|
||||
from .common import load_config_from_fixture
|
||||
|
||||
# Pre-compiled regex patterns for extracting object IDs from expressions
|
||||
# Matches both old format: .set_object_id("obj_id")
|
||||
# and new format: .set_name_and_object_id("name", "obj_id")
|
||||
OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)')
|
||||
COMBINED_PATTERN = re.compile(
|
||||
r'\.set_name_and_object_id\(["\'].*?["\']\s*,\s*["\'](.*?)["\']\)'
|
||||
)
|
||||
# Pre-compiled regex pattern for extracting names from set_name calls
|
||||
# Matches: .set_name("name", hash) or .set_name("name")
|
||||
SET_NAME_PATTERN = re.compile(r'\.set_name\(["\']([^"\']*)["\']')
|
||||
|
||||
FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers"
|
||||
|
||||
@@ -276,14 +272,21 @@ def setup_test_environment() -> Generator[list[str], None, None]:
|
||||
|
||||
|
||||
def extract_object_id_from_expressions(expressions: list[str]) -> str | None:
|
||||
"""Extract the object ID that was set from the generated expressions."""
|
||||
"""Extract the object ID that would be computed from set_name calls.
|
||||
|
||||
Since object_id is now computed from the name (via snake_case + sanitize),
|
||||
we extract the name from set_name() calls and compute the expected object_id.
|
||||
For empty names, we fall back to CORE.friendly_name or CORE.name.
|
||||
"""
|
||||
for expr in expressions:
|
||||
# First try new combined format: .set_name_and_object_id("name", "obj_id")
|
||||
if match := COMBINED_PATTERN.search(expr):
|
||||
return match.group(1)
|
||||
# Fall back to old format: .set_object_id("obj_id")
|
||||
if match := OBJECT_ID_PATTERN.search(expr):
|
||||
return match.group(1)
|
||||
if match := SET_NAME_PATTERN.search(expr):
|
||||
name = match.group(1)
|
||||
if name:
|
||||
return sanitize(snake_case(name))
|
||||
# Empty name - fall back to friendly_name or device name
|
||||
if CORE.friendly_name:
|
||||
return sanitize(snake_case(CORE.friendly_name))
|
||||
return sanitize(snake_case(CORE.name)) if CORE.name else None
|
||||
return None
|
||||
|
||||
|
||||
@@ -757,3 +760,140 @@ def test_entity_duplicate_validator_same_name_no_enhanced_message() -> None:
|
||||
r"Each entity on a device must have a unique name within its platform\.$",
|
||||
):
|
||||
validator(config2)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_entity_empty_name_with_device(
|
||||
setup_test_environment: list[str],
|
||||
) -> None:
|
||||
"""Test setup_entity with empty entity name on a sub-device.
|
||||
|
||||
For empty-name entities, Python passes 0 and C++ calculates the hash
|
||||
at runtime from the device's actual name.
|
||||
"""
|
||||
added_expressions = setup_test_environment
|
||||
|
||||
# Mock get_variable to return a mock device
|
||||
original_get_variable = entity_helpers.get_variable
|
||||
|
||||
async def mock_get_variable(id_: ID) -> MockObj:
|
||||
return MockObj("sub_device_1")
|
||||
|
||||
entity_helpers.get_variable = mock_get_variable
|
||||
|
||||
var = MockObj("sensor1")
|
||||
device_id = ID("sub_device_1", type="Device")
|
||||
|
||||
config = {
|
||||
CONF_NAME: "",
|
||||
CONF_DISABLED_BY_DEFAULT: False,
|
||||
CONF_DEVICE_ID: device_id,
|
||||
}
|
||||
|
||||
await setup_entity(var, config, "sensor")
|
||||
|
||||
entity_helpers.get_variable = original_get_variable
|
||||
|
||||
# Check that set_device was called
|
||||
assert any("sensor1.set_device" in expr for expr in added_expressions)
|
||||
|
||||
# For empty-name entities, Python passes 0 - C++ calculates hash at runtime
|
||||
assert any('set_name("", 0)' in expr for expr in added_expressions), (
|
||||
f"Expected set_name with hash 0, got {added_expressions}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_entity_empty_name_with_mac_suffix(
|
||||
setup_test_environment: list[str],
|
||||
) -> None:
|
||||
"""Test setup_entity with empty name and MAC suffix enabled.
|
||||
|
||||
For empty-name entities, Python passes 0 and C++ calculates the hash
|
||||
at runtime from friendly_name (bug-for-bug compatibility).
|
||||
"""
|
||||
added_expressions = setup_test_environment
|
||||
|
||||
# Set up CORE.config with name_add_mac_suffix enabled
|
||||
CORE.config = {"name_add_mac_suffix": True}
|
||||
# Set friendly_name to a specific value
|
||||
CORE.friendly_name = "My Device"
|
||||
|
||||
var = MockObj("sensor1")
|
||||
|
||||
config = {
|
||||
CONF_NAME: "",
|
||||
CONF_DISABLED_BY_DEFAULT: False,
|
||||
}
|
||||
|
||||
await setup_entity(var, config, "sensor")
|
||||
|
||||
# For empty-name entities, Python passes 0 - C++ calculates hash at runtime
|
||||
assert any('set_name("", 0)' in expr for expr in added_expressions), (
|
||||
f"Expected set_name with hash 0, got {added_expressions}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name(
|
||||
setup_test_environment: list[str],
|
||||
) -> None:
|
||||
"""Test setup_entity with empty name, MAC suffix enabled, but no friendly_name.
|
||||
|
||||
For empty-name entities, Python passes 0 and C++ calculates the hash
|
||||
at runtime. In this case C++ will hash the empty friendly_name
|
||||
(bug-for-bug compatibility).
|
||||
"""
|
||||
added_expressions = setup_test_environment
|
||||
|
||||
# Set up CORE.config with name_add_mac_suffix enabled
|
||||
CORE.config = {"name_add_mac_suffix": True}
|
||||
# Set friendly_name to empty
|
||||
CORE.friendly_name = ""
|
||||
|
||||
var = MockObj("sensor1")
|
||||
|
||||
config = {
|
||||
CONF_NAME: "",
|
||||
CONF_DISABLED_BY_DEFAULT: False,
|
||||
}
|
||||
|
||||
await setup_entity(var, config, "sensor")
|
||||
|
||||
# For empty-name entities, Python passes 0 - C++ calculates hash at runtime
|
||||
assert any('set_name("", 0)' in expr for expr in added_expressions), (
|
||||
f"Expected set_name with hash 0, got {added_expressions}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name(
|
||||
setup_test_environment: list[str],
|
||||
) -> None:
|
||||
"""Test setup_entity with empty name, no MAC suffix, and no friendly_name.
|
||||
|
||||
For empty-name entities, Python passes 0 and C++ calculates the hash
|
||||
at runtime from the device name.
|
||||
"""
|
||||
added_expressions = setup_test_environment
|
||||
|
||||
# No MAC suffix (either not set or False)
|
||||
CORE.config = {}
|
||||
# No friendly_name
|
||||
CORE.friendly_name = ""
|
||||
# Device name is set
|
||||
CORE.name = "my-test-device"
|
||||
|
||||
var = MockObj("sensor1")
|
||||
|
||||
config = {
|
||||
CONF_NAME: "",
|
||||
CONF_DISABLED_BY_DEFAULT: False,
|
||||
}
|
||||
|
||||
await setup_entity(var, config, "sensor")
|
||||
|
||||
# For empty-name entities, Python passes 0 - C++ calculates hash at runtime
|
||||
assert any('set_name("", 0)' in expr for expr in added_expressions), (
|
||||
f"Expected set_name with hash 0, got {added_expressions}"
|
||||
)
|
||||
|
||||
@@ -279,6 +279,77 @@ def test_sanitize(text, expected):
|
||||
assert actual == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "expected_hash"),
|
||||
(
|
||||
# Basic strings - hash of sanitize(snake_case(name))
|
||||
("foo", 0x408F5E13),
|
||||
("Foo", 0x408F5E13), # Same as "foo" (lowercase)
|
||||
("FOO", 0x408F5E13), # Same as "foo" (lowercase)
|
||||
# Spaces become underscores
|
||||
("foo bar", 0x3AE35AA1), # Transforms to "foo_bar"
|
||||
("Foo Bar", 0x3AE35AA1), # Same (lowercase + underscore)
|
||||
# Already snake_case
|
||||
("foo_bar", 0x3AE35AA1),
|
||||
# Special chars become underscores
|
||||
("foo!bar", 0x3AE35AA1), # Transforms to "foo_bar"
|
||||
("foo@bar", 0x3AE35AA1), # Transforms to "foo_bar"
|
||||
# Hyphens are preserved
|
||||
("foo-bar", 0x438B12E3),
|
||||
# Numbers are preserved
|
||||
("foo123", 0xF3B0067D),
|
||||
# Empty string
|
||||
("", 0x811C9DC5), # FNV1_OFFSET_BASIS (no chars processed)
|
||||
# Single char
|
||||
("a", 0x050C5D7E),
|
||||
# Mixed case and spaces
|
||||
("My Sensor Name", 0x2760962A), # Transforms to "my_sensor_name"
|
||||
),
|
||||
)
|
||||
def test_fnv1_hash_object_id(name, expected_hash):
|
||||
"""Test fnv1_hash_object_id produces expected hashes.
|
||||
|
||||
These expected values were computed to match the C++ implementation
|
||||
in esphome/core/helpers.h. If this test fails after modifying either
|
||||
implementation, ensure both Python and C++ versions stay in sync.
|
||||
"""
|
||||
actual = helpers.fnv1_hash_object_id(name)
|
||||
|
||||
assert actual == expected_hash
|
||||
|
||||
|
||||
def _fnv1_hash_py(s: str) -> int:
|
||||
"""Python implementation of FNV-1 hash for verification."""
|
||||
hash_val = 2166136261 # FNV1_OFFSET_BASIS
|
||||
for c in s:
|
||||
hash_val = (hash_val * 16777619) & 0xFFFFFFFF # FNV1_PRIME
|
||||
hash_val ^= ord(c)
|
||||
return hash_val
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
(
|
||||
"Simple",
|
||||
"With Space",
|
||||
"MixedCase",
|
||||
"special!@#chars",
|
||||
"already_snake_case",
|
||||
"123numbers",
|
||||
),
|
||||
)
|
||||
def test_fnv1_hash_object_id_matches_manual_calculation(name):
|
||||
"""Verify fnv1_hash_object_id matches snake_case + sanitize + standard FNV-1."""
|
||||
# Manual calculation: snake_case -> sanitize -> fnv1_hash
|
||||
transformed = helpers.sanitize(helpers.snake_case(name))
|
||||
expected = _fnv1_hash_py(transformed)
|
||||
|
||||
# Direct calculation via fnv1_hash_object_id
|
||||
actual = helpers.fnv1_hash_object_id(name)
|
||||
|
||||
assert actual == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, expected",
|
||||
((["127.0.0.1", "fe80::1", "2001::2"], ["2001::2", "127.0.0.1", "fe80::1"]),),
|
||||
|
||||
Reference in New Issue
Block a user