Merge branch 'protect_entity_setters' into integration

This commit is contained in:
J. Nick Koston
2026-03-06 09:56:13 -10:00
30 changed files with 503 additions and 200 deletions
+2 -2
View File
@@ -619,7 +619,7 @@ void ATM90E32Component::run_gain_calibrations() {
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs, ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs,
phase_labels[phase]); phase_labels[phase]);
} else { } else {
uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain); uint32_t new_voltage_gain = static_cast<uint32_t>((ref_voltage / measured_voltage) * current_voltage_gain);
if (new_voltage_gain == 0) { if (new_voltage_gain == 0) {
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs, ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs,
phase_labels[phase]); phase_labels[phase]);
@@ -644,7 +644,7 @@ void ATM90E32Component::run_gain_calibrations() {
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs, ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs,
phase_labels[phase]); phase_labels[phase]);
} else { } else {
uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain); uint32_t new_current_gain = static_cast<uint32_t>((ref_current / measured_current) * current_current_gain);
if (new_current_gain == 0) { if (new_current_gain == 0) {
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs, ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs,
phase_labels[phase]); phase_labels[phase]);
+4 -4
View File
@@ -164,10 +164,10 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo
std::unique_ptr<BedjetCodec> codec_; std::unique_ptr<BedjetCodec> codec_;
bool discover_characteristics_(); bool discover_characteristics_();
uint16_t char_handle_cmd_; uint16_t char_handle_cmd_{0};
uint16_t char_handle_name_; uint16_t char_handle_name_{0};
uint16_t char_handle_status_; uint16_t char_handle_status_{0};
uint16_t config_descr_status_; uint16_t config_descr_status_{0};
uint8_t write_notify_config_descriptor_(bool enable); uint8_t write_notify_config_descriptor_(bool enable);
}; };
@@ -67,21 +67,21 @@ class CurrentBasedCover : public cover::Cover, public Component {
sensor::Sensor *open_sensor_{nullptr}; sensor::Sensor *open_sensor_{nullptr};
Trigger<> open_trigger_; Trigger<> open_trigger_;
float open_moving_current_threshold_; float open_moving_current_threshold_{0.0f};
float open_obstacle_current_threshold_{FLT_MAX}; float open_obstacle_current_threshold_{FLT_MAX};
uint32_t open_duration_; uint32_t open_duration_{0};
sensor::Sensor *close_sensor_{nullptr}; sensor::Sensor *close_sensor_{nullptr};
Trigger<> close_trigger_; Trigger<> close_trigger_;
float close_moving_current_threshold_; float close_moving_current_threshold_{0.0f};
float close_obstacle_current_threshold_{FLT_MAX}; float close_obstacle_current_threshold_{FLT_MAX};
uint32_t close_duration_; uint32_t close_duration_{0};
uint32_t max_duration_{UINT32_MAX}; uint32_t max_duration_{UINT32_MAX};
bool malfunction_detection_{true}; bool malfunction_detection_{true};
Trigger<> malfunction_trigger_; Trigger<> malfunction_trigger_;
uint32_t start_sensing_delay_; uint32_t start_sensing_delay_{0};
float obstacle_rollback_; float obstacle_rollback_{0.0f};
Trigger<> *prev_command_trigger_{nullptr}; Trigger<> *prev_command_trigger_{nullptr};
uint32_t last_recompute_time_{0}; uint32_t last_recompute_time_{0};
+1 -1
View File
@@ -91,7 +91,7 @@ class DateCall {
DateEntity *parent_; DateEntity *parent_;
optional<int16_t> year_; optional<uint16_t> year_;
optional<uint8_t> month_; optional<uint8_t> month_;
optional<uint8_t> day_; optional<uint8_t> day_;
}; };
@@ -145,7 +145,7 @@ class DeepSleepComponent : public Component {
#endif // USE_BK72XX #endif // USE_BK72XX
#ifdef USE_ESP32 #ifdef USE_ESP32
InternalGPIOPin *wakeup_pin_; InternalGPIOPin *wakeup_pin_{nullptr};
WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE};
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
+1 -1
View File
@@ -172,7 +172,7 @@ uint8_t ES7210::es7210_gain_reg_value_(float mic_gain) {
// reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB // reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB
mic_gain += 0.5; mic_gain += 0.5;
if (mic_gain <= 33.0) { if (mic_gain <= 33.0) {
return (uint8_t) mic_gain / 3; return (uint8_t) (mic_gain / 3);
} }
if (mic_gain < 36.0) { if (mic_gain < 36.0) {
return 12; return 12;
@@ -7,6 +7,7 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <algorithm>
#include <nvs_flash.h> #include <nvs_flash.h>
#include <freertos/FreeRTOSConfig.h> #include <freertos/FreeRTOSConfig.h>
#include <esp_bt_main.h> #include <esp_bt_main.h>
@@ -38,21 +39,16 @@ void BLEServer::loop() {
case RUNNING: { case RUNNING: {
// Start all services that are pending to start // Start all services that are pending to start
if (!this->services_to_start_.empty()) { if (!this->services_to_start_.empty()) {
uint16_t index_to_remove = 0; for (auto &service : this->services_to_start_) {
// Iterate over the services to start
for (unsigned i = 0; i < this->services_to_start_.size(); i++) {
BLEService *service = this->services_to_start_[i];
if (service->is_created()) { if (service->is_created()) {
service->start(); // Needs to be called once per characteristic in the service service->start(); // Needs to be called once per characteristic in the service
} else {
index_to_remove = i + 1;
} }
} }
// Remove the services that have been started // Remove services that have been started
if (index_to_remove > 0) { this->services_to_start_.erase(
this->services_to_start_.erase(this->services_to_start_.begin(), std::remove_if(this->services_to_start_.begin(), this->services_to_start_.end(),
this->services_to_start_.begin() + index_to_remove - 1); [](BLEService *service) { return service->is_starting() || service->is_running(); }),
} this->services_to_start_.end());
} }
break; break;
} }
+2 -2
View File
@@ -138,7 +138,7 @@ class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *,
protected: protected:
bool has_address_{false}; bool has_address_{false};
const uint8_t *address_[ESP_NOW_ETH_ALEN]; uint8_t address_[ESP_NOW_ETH_ALEN];
}; };
class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>, class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowUnknownPeerHandler { public ESPNowUnknownPeerHandler {
@@ -167,7 +167,7 @@ class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_
protected: protected:
bool has_address_{false}; bool has_address_{false};
const uint8_t *address_[ESP_NOW_ETH_ALEN]; uint8_t address_[ESP_NOW_ETH_ALEN];
}; };
} // namespace esphome::espnow } // namespace esphome::espnow
@@ -171,12 +171,27 @@ class AddressableScanEffect : public AddressableLightEffect {
if (now - this->last_move_ < this->move_interval_) if (now - this->last_move_ < this->move_interval_)
return; return;
if (direction_) { const auto num_leds = static_cast<uint32_t>(it.size());
if (this->scan_width_ >= num_leds) {
it.all() = current_color;
it.schedule_show();
this->last_move_ = now;
return;
}
const uint32_t max_pos = num_leds - this->scan_width_;
if (this->at_led_ >= max_pos) {
this->at_led_ = max_pos;
this->direction_ = false;
}
if (this->direction_) {
this->at_led_++; this->at_led_++;
if (this->at_led_ == it.size() - this->scan_width_) if (this->at_led_ >= max_pos)
this->direction_ = false; this->direction_ = false;
} else { } else {
this->at_led_--; if (this->at_led_ > 0)
this->at_led_--;
if (this->at_led_ == 0) if (this->at_led_ == 0)
this->direction_ = true; this->direction_ = true;
} }
+2 -2
View File
@@ -63,8 +63,8 @@ class MAX6956 : public Component, public i2c::I2CDevice {
bool read_reg_(uint8_t reg, uint8_t *value); bool read_reg_(uint8_t reg, uint8_t *value);
// write a value to a given register // write a value to a given register
bool write_reg_(uint8_t reg, uint8_t value); bool write_reg_(uint8_t reg, uint8_t value);
max6956::MAX6956CURRENTMODE brightness_mode_; max6956::MAX6956CURRENTMODE brightness_mode_{};
uint8_t global_brightness_; uint8_t global_brightness_{0};
private: private:
int8_t prev_bright_[28] = {0}; int8_t prev_bright_[28] = {0};
+4 -4
View File
@@ -67,9 +67,9 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice {
/// use raw temperature & pressure to calculate & publish values /// use raw temperature & pressure to calculate & publish values
void calculate_values_(uint32_t raw_temperature, uint32_t raw_pressure); void calculate_values_(uint32_t raw_temperature, uint32_t raw_pressure);
sensor::Sensor *temperature_sensor_; sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_; sensor::Sensor *pressure_sensor_{nullptr};
sensor::Sensor *humidity_sensor_; sensor::Sensor *humidity_sensor_{nullptr};
/** I2CDevice object to communicate with secondary I2C address for the humidity sensor /** I2CDevice object to communicate with secondary I2C address for the humidity sensor
* *
@@ -77,7 +77,7 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice {
* *
* Default address for humidity is 0x40 * Default address for humidity is 0x40
*/ */
MS8607HumidityDevice *humidity_device_; MS8607HumidityDevice *humidity_device_{nullptr};
/// This device's pressure & temperature calibration values, read from PROM /// This device's pressure & temperature calibration values, read from PROM
struct CalibrationValues { struct CalibrationValues {
+62 -68
View File
@@ -118,15 +118,15 @@ void NoblexClimate::transmit_state() {
data->mark(NOBLEX_HEADER_MARK); data->mark(NOBLEX_HEADER_MARK);
data->space(NOBLEX_HEADER_SPACE); data->space(NOBLEX_HEADER_SPACE);
// Data (sent remote_state from the MSB to the LSB) // Data (sent remote_state from the MSB to the LSB)
for (uint8_t i : remote_state) { for (int byte_idx = 0; byte_idx < 8; byte_idx++) {
for (int8_t j = 7; j >= 0; j--) { for (int8_t bit_idx = 7; bit_idx >= 0; bit_idx--) {
if ((i == 4) & (j == 4)) { if ((byte_idx == 4) && (bit_idx == 4)) {
// Header intermediate // Header intermediate
data->mark(NOBLEX_BIT_MARK); data->mark(NOBLEX_BIT_MARK);
data->space(NOBLEX_GAP); // gap en bit 36 data->space(NOBLEX_GAP); // gap en bit 36
} else { } else {
data->mark(NOBLEX_BIT_MARK); data->mark(NOBLEX_BIT_MARK);
bool bit = i & (1 << j); bool bit = remote_state[byte_idx] & (1 << bit_idx);
data->space(bit ? NOBLEX_ONE_SPACE : NOBLEX_ZERO_SPACE); data->space(bit ? NOBLEX_ONE_SPACE : NOBLEX_ZERO_SPACE);
} }
} }
@@ -145,76 +145,71 @@ void NoblexClimate::transmit_state() {
// Handle received IR Buffer // Handle received IR Buffer
bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) {
uint8_t remote_state[8] = {0}; if (data.peek_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) {
uint8_t crc = 0, crc_calculated = 0; // First part: header + first 36 bits, followed by 20ms gap
data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE);
if (!receiving_) { ESP_LOGV(TAG, "Header");
// Validate header this->receiving_ = false;
if (data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) { memset(this->remote_state_, 0, sizeof(this->remote_state_));
ESP_LOGV(TAG, "Header"); for (int i = 0; i < 5; i++) {
receiving_ = true;
// Read first 36 bits
for (int i = 0; i < 5; i++) {
// Read bit
for (int j = 7; j >= 0; j--) {
if ((i == 4) & (j == 4)) {
remote_state[i] |= 1 << j;
// Header intermediate
ESP_LOGVV(TAG, "GAP");
return false;
} else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) {
remote_state[i] |= 1 << j;
} else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) {
ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j);
return false;
}
}
ESP_LOGV(TAG, "Byte %d %02X", i, remote_state[i]);
}
} else {
ESP_LOGV(TAG, "Header fail");
receiving_ = false;
return false;
}
} else {
// Read the remaining 28 bits
for (int i = 4; i < 8; i++) {
// Read bit
for (int j = 7; j >= 0; j--) { for (int j = 7; j >= 0; j--) {
if ((i == 4) & (j >= 4)) { if ((i == 4) && (j == 4)) {
// nothing this->remote_state_[i] |= 1 << j;
ESP_LOGVV(TAG, "GAP");
this->receiving_ = true;
return false;
} else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) {
remote_state[i] |= 1 << j; this->remote_state_[i] |= 1 << j;
} else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) {
ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j);
return false; return false;
} }
} }
ESP_LOGV(TAG, "Byte %d %02X", i, remote_state[i]); ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]);
} }
return false;
}
// Read crc // Second part: remaining 28 bits + 4-bit CRC + footer
for (int i = 3; i >= 0; i--) { if (!this->receiving_) {
if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { return false;
crc |= 1 << i; }
this->receiving_ = false;
for (int i = 4; i < 8; i++) {
for (int j = 7; j >= 0; j--) {
if ((i == 4) && (j >= 4)) {
// already decoded in first part
} else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) {
this->remote_state_[i] |= 1 << j;
} else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) {
ESP_LOGVV(TAG, "Bit %d CRC fail", i); ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j);
return false; return false;
} }
} }
ESP_LOGV(TAG, "CRC %02X", crc); ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]);
// Validate footer
if (!data.expect_mark(NOBLEX_BIT_MARK)) {
ESP_LOGV(TAG, "Footer fail");
return false;
}
receiving_ = false;
} }
for (uint8_t i : remote_state) // Read CRC
uint8_t crc = 0;
for (int i = 3; i >= 0; i--) {
if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) {
crc |= 1 << i;
} else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) {
ESP_LOGVV(TAG, "Bit %d CRC fail", i);
return false;
}
}
ESP_LOGV(TAG, "CRC %02X", crc);
// Validate footer
if (!data.expect_mark(NOBLEX_BIT_MARK)) {
ESP_LOGV(TAG, "Footer fail");
return false;
}
// Validate CRC
uint8_t crc_calculated = 0;
for (uint8_t i : this->remote_state_)
crc_calculated += reverse_bits(i); crc_calculated += reverse_bits(i);
crc_calculated = reverse_bits(uint8_t(crc_calculated & 0x0F)) >> 4; crc_calculated = reverse_bits(uint8_t(crc_calculated & 0x0F)) >> 4;
ESP_LOGVV(TAG, "CRC calc %02X", crc_calculated); ESP_LOGVV(TAG, "CRC calc %02X", crc_calculated);
@@ -224,11 +219,12 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) {
return false; return false;
} }
ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", remote_state[0], remote_state[1], ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", this->remote_state_[0],
remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], remote_state[7]); this->remote_state_[1], this->remote_state_[2], this->remote_state_[3], this->remote_state_[4],
this->remote_state_[5], this->remote_state_[6], this->remote_state_[7]);
auto powered_on = false; auto powered_on = false;
if ((remote_state[0] & NOBLEX_POWER) == NOBLEX_POWER) { if ((this->remote_state_[0] & NOBLEX_POWER) == NOBLEX_POWER) {
powered_on = true; powered_on = true;
this->powered_on_assumed = powered_on; this->powered_on_assumed = powered_on;
} else { } else {
@@ -241,7 +237,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) {
// Set received mode // Set received mode
if (powered_on_assumed) { if (powered_on_assumed) {
auto mode = (remote_state[0] & 0xE0) >> 5; auto mode = (this->remote_state_[0] & 0xE0) >> 5;
ESP_LOGV(TAG, "Mode: %02X", mode); ESP_LOGV(TAG, "Mode: %02X", mode);
switch (mode) { switch (mode) {
case IRNoblexMode::IR_NOBLEX_MODE_AUTO: case IRNoblexMode::IR_NOBLEX_MODE_AUTO:
@@ -263,7 +259,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) {
} }
// Set received temp // Set received temp
uint8_t temp = remote_state[1]; uint8_t temp = this->remote_state_[1];
ESP_LOGVV(TAG, "Temperature Raw: %02X", temp); ESP_LOGVV(TAG, "Temperature Raw: %02X", temp);
temp = 0x0F & reverse_bits(temp); temp = 0x0F & reverse_bits(temp);
@@ -272,7 +268,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) {
this->target_temperature = temp; this->target_temperature = temp;
// Set received fan speed // Set received fan speed
auto fan = (remote_state[0] & 0x0C) >> 2; auto fan = (this->remote_state_[0] & 0x0C) >> 2;
ESP_LOGV(TAG, "Fan: %02X", fan); ESP_LOGV(TAG, "Fan: %02X", fan);
switch (fan) { switch (fan) {
case IRNoblexFan::IR_NOBLEX_FAN_HIGH: case IRNoblexFan::IR_NOBLEX_FAN_HIGH:
@@ -291,7 +287,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) {
} }
// Set received swing status // Set received swing status
if (remote_state[0] & 0x02) { if (this->remote_state_[0] & 0x02) {
ESP_LOGV(TAG, "Swing vertical"); ESP_LOGV(TAG, "Swing vertical");
this->swing_mode = climate::CLIMATE_SWING_VERTICAL; this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
} else { } else {
@@ -299,8 +295,6 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) {
this->swing_mode = climate::CLIMATE_SWING_OFF; this->swing_mode = climate::CLIMATE_SWING_OFF;
} }
for (uint8_t &i : remote_state)
i = 0;
this->publish_state(); this->publish_state();
return true; return true;
} // end on_receive() } // end on_receive()
+2 -1
View File
@@ -41,7 +41,8 @@ class NoblexClimate : public climate_ir::ClimateIR {
/// Handle received IR Buffer. /// Handle received IR Buffer.
bool on_receive(remote_base::RemoteReceiveData data) override; bool on_receive(remote_base::RemoteReceiveData data) override;
bool send_swing_cmd_{false}; bool send_swing_cmd_{false};
bool receiving_ = false; bool receiving_{false};
uint8_t remote_state_[8]{};
}; };
} // namespace noblex } // namespace noblex
@@ -96,7 +96,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
bool inverted_{false}; bool inverted_{false};
bool non_blocking_{false}; bool non_blocking_{false};
#endif #endif
uint8_t carrier_duty_percent_; uint8_t carrier_duty_percent_{50};
Trigger<> transmit_trigger_; Trigger<> transmit_trigger_;
Trigger<> complete_trigger_; Trigger<> complete_trigger_;
@@ -70,7 +70,7 @@ void RP2040PIOLEDStripLightOutput::setup() {
// but there are only 4 state machines on each PIO so we can only have 4 strips per PIO // but there are only 4 state machines on each PIO so we can only have 4 strips per PIO
uint offset = 0; uint offset = 0;
if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] >= 4) {
ESP_LOGE(TAG, "Too many instances of PIO program"); ESP_LOGE(TAG, "Too many instances of PIO program");
this->mark_failed(); this->mark_failed();
return; return;
+4 -4
View File
@@ -104,12 +104,12 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
char serial_number_[17] = "UNKNOWN"; char serial_number_[17] = "UNKNOWN";
uint16_t voc_baseline_state_[4]{0}; uint16_t voc_baseline_state_[4]{0};
uint32_t voc_baseline_time_; uint32_t voc_baseline_time_{0};
uint16_t firmware_version_; uint16_t firmware_version_{0};
Sen5xType type_{Sen5xType::UNKNOWN}; Sen5xType type_{Sen5xType::UNKNOWN};
ERRORCODE error_code_; ERRORCODE error_code_{ERRORCODE::UNKNOWN};
bool initialized_{false}; bool initialized_{false};
bool store_baseline_; bool store_baseline_{false};
sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_1_0_sensor_{nullptr};
sensor::Sensor *pm_2_5_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr};
+1 -1
View File
@@ -199,7 +199,7 @@ void SGP4xComponent::measure_raw_() {
response_words = 2; response_words = 2;
} }
} }
uint16_t rhticks = llround((uint16_t) ((humidity * 65535) / 100)); uint16_t rhticks = (uint16_t) llround((humidity * 65535) / 100);
uint16_t tempticks = (uint16_t) (((temperature + 45) * 65535) / 175); uint16_t tempticks = (uint16_t) (((temperature + 45) * 65535) / 175);
// first parameter are the relative humidity ticks // first parameter are the relative humidity ticks
data[0] = rhticks; data[0] = rhticks;
+5 -5
View File
@@ -107,11 +107,11 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent {
std::string recipient_; std::string recipient_;
std::string outgoing_message_; std::string outgoing_message_;
std::string ussd_; std::string ussd_;
bool send_pending_; bool send_pending_{false};
bool dial_pending_; bool dial_pending_{false};
bool connect_pending_; bool connect_pending_{false};
bool disconnect_pending_; bool disconnect_pending_{false};
bool send_ussd_pending_; bool send_ussd_pending_{false};
uint8_t call_state_{6}; uint8_t call_state_{6};
CallbackManager<void(std::string, std::string)> sms_received_callback_; CallbackManager<void(std::string, std::string)> sms_received_callback_;
+1 -1
View File
@@ -90,7 +90,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
}; };
struct timezone tz = {0, 0}; struct timezone tz = {0, 0};
int ret = settimeofday(&timev, &tz); int ret = settimeofday(&timev, &tz);
if (ret == EINVAL) { if (ret != 0 && errno == EINVAL) {
// Some ESP8266 frameworks abort when timezone parameter is not NULL // Some ESP8266 frameworks abort when timezone parameter is not NULL
// while ESP32 expects it not to be NULL // while ESP32 expects it not to be NULL
ret = settimeofday(&timev, nullptr); ret = settimeofday(&timev, nullptr);
+16 -13
View File
@@ -147,35 +147,38 @@ void TM1638Component::set_intensity(uint8_t brightness_level) {
uint8_t TM1638Component::print(uint8_t start_pos, const char *str) { uint8_t TM1638Component::print(uint8_t start_pos, const char *str) {
uint8_t pos = start_pos; uint8_t pos = start_pos;
bool last_was_dot = false; bool last_was_dot = false;
for (; *str != '\0'; str++) { for (; *str != '\0'; str++) {
uint8_t data = TM1638_UNKNOWN_CHAR; uint8_t data = TM1638_UNKNOWN_CHAR;
if (*str >= ' ' && *str <= '~') { if (*str >= ' ' && *str <= '~') {
data = progmem_read_byte(&TM1638Translation::SEVEN_SEG[*str - 32]); // subract 32 to account for ASCII offset // Subtract 32 to account for ASCII offset
} else if (data == TM1638_UNKNOWN_CHAR) { data = progmem_read_byte(&TM1638Translation::SEVEN_SEG[*str - 32]);
} else {
ESP_LOGW(TAG, "Encountered character '%c' with no TM1638 representation while translating string!", *str); ESP_LOGW(TAG, "Encountered character '%c' with no TM1638 representation while translating string!", *str);
} }
if (*str == '.') // handle dots if (*str == '.') {
{ // Merge dot onto previous character unless we're at the start or last was also a dot
if (pos != start_pos && if (pos != start_pos && !last_was_dot) {
!last_was_dot) // if we are not at the first position, backup by one unless last char was a dot
{
pos--; pos--;
} }
this->buffer_[pos] |= 0b10000000; // turn on the dot on the previous position if (pos >= 8) {
last_was_dot = true; // set a bit in case the next chracter is also a dot ESP_LOGI(TAG, "TM1638 String is too long for the display!");
} else // if not a dot, then just write the character to display break;
{ }
// Turn on the dot on the previous position
this->buffer_[pos] |= 0b10000000;
last_was_dot = true;
} else {
// Not a dot, write the character to display
if (pos >= 8) { if (pos >= 8) {
ESP_LOGI(TAG, "TM1638 String is too long for the display!"); ESP_LOGI(TAG, "TM1638 String is too long for the display!");
break; break;
} }
this->buffer_[pos] = data; this->buffer_[pos] = data;
last_was_dot = false; // clear dot tracking bit last_was_dot = false;
} }
pos++; pos++;
+3 -1
View File
@@ -327,7 +327,9 @@ uint16_t TSL2591Component::get_illuminance(TSL2591SensorChannel channel, uint32_
return (combined_illuminance >> 16); return (combined_illuminance >> 16);
} else if (channel == TSL2591_SENSOR_CHANNEL_VISIBLE) { } else if (channel == TSL2591_SENSOR_CHANNEL_VISIBLE) {
// Reads all and subtracts out the infrared // Reads all and subtracts out the infrared
return ((combined_illuminance & 0xFFFF) - (combined_illuminance >> 16)); uint16_t full = combined_illuminance & 0xFFFF;
uint16_t ir = combined_illuminance >> 16;
return (ir > full) ? 0 : (full - ir);
} }
// unknown channel! // unknown channel!
ESP_LOGE(TAG, "get_illuminance() caller requested an unknown channel: %d", channel); ESP_LOGE(TAG, "get_illuminance() caller requested an unknown channel: %d", channel);
+8 -6
View File
@@ -11,7 +11,7 @@ static const char *const TAG = "entity_base";
// Entity Name // Entity Name
const StringRef &EntityBase::get_name() const { return this->name_; } const StringRef &EntityBase::get_name() const { return this->name_; }
void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed) { void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields) {
this->name_ = StringRef(name); this->name_ = StringRef(name);
if (this->name_.empty()) { if (this->name_.empty()) {
#ifdef USE_DEVICES #ifdef USE_DEVICES
@@ -44,17 +44,19 @@ void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, ui
this->calc_object_id_(); this->calc_object_id_();
} }
} }
// Unpack entity string table indices. // Unpack entity string table indices and flags from entity_fields.
// Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits)
#ifdef USE_ENTITY_DEVICE_CLASS #ifdef USE_ENTITY_DEVICE_CLASS
this->device_class_idx_ = entity_strings_packed & 0xFF; this->device_class_idx_ = (entity_fields >> ENTITY_FIELD_DC_SHIFT) & 0xFF;
#endif #endif
#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT #ifdef USE_ENTITY_UNIT_OF_MEASUREMENT
this->uom_idx_ = (entity_strings_packed >> 8) & 0xFF; this->uom_idx_ = (entity_fields >> ENTITY_FIELD_UOM_SHIFT) & 0xFF;
#endif #endif
#ifdef USE_ENTITY_ICON #ifdef USE_ENTITY_ICON
this->icon_idx_ = (entity_strings_packed >> 16) & 0xFF; this->icon_idx_ = (entity_fields >> ENTITY_FIELD_ICON_SHIFT) & 0xFF;
#endif #endif
this->flags_.internal = (entity_fields >> ENTITY_FIELD_INTERNAL_SHIFT) & 1;
this->flags_.disabled_by_default = (entity_fields >> ENTITY_FIELD_DISABLED_BY_DEFAULT_SHIFT) & 1;
this->flags_.entity_category = (entity_fields >> ENTITY_FIELD_ENTITY_CATEGORY_SHIFT) & 0x3;
} }
// Weak default lookup functions — overridden by generated code in main.cpp // Weak default lookup functions — overridden by generated code in main.cpp
+20 -11
View File
@@ -55,6 +55,15 @@ enum EntityCategory : uint8_t {
ENTITY_CATEGORY_DIAGNOSTIC = 2, ENTITY_CATEGORY_DIAGNOSTIC = 2,
}; };
// Bit layout for entity_fields parameter in configure_entity_().
// Keep in sync with _*_SHIFT constants in esphome/core/entity_helpers.py
static constexpr uint8_t ENTITY_FIELD_DC_SHIFT = 0;
static constexpr uint8_t ENTITY_FIELD_UOM_SHIFT = 8;
static constexpr uint8_t ENTITY_FIELD_ICON_SHIFT = 16;
static constexpr uint8_t ENTITY_FIELD_INTERNAL_SHIFT = 24;
static constexpr uint8_t ENTITY_FIELD_DISABLED_BY_DEFAULT_SHIFT = 25;
static constexpr uint8_t ENTITY_FIELD_ENTITY_CATEGORY_SHIFT = 26;
// The generic Entity base class that provides an interface common to all Entities. // The generic Entity base class that provides an interface common to all Entities.
class EntityBase { class EntityBase {
public: public:
@@ -88,21 +97,16 @@ class EntityBase {
/// Useful for building compound strings without intermediate buffer /// Useful for building compound strings without intermediate buffer
size_t write_object_id_to(char *buf, size_t buf_size) const; size_t write_object_id_to(char *buf, size_t buf_size) const;
// Get/set whether this Entity should be hidden outside ESPHome // Get whether this Entity should be hidden outside ESPHome
bool is_internal() const { return this->flags_.internal; } bool is_internal() const { return this->flags_.internal; }
void set_internal(bool internal) { this->flags_.internal = internal; }
// Check if this object is declared to be disabled by default. // Check if this object is declared to be disabled by default.
// That means that when the device gets added to Home Assistant (or other clients) it should // That means that when the device gets added to Home Assistant (or other clients) it should
// not be added to the default view by default, and a user action is necessary to manually add it. // not be added to the default view by default, and a user action is necessary to manually add it.
bool is_disabled_by_default() const { return this->flags_.disabled_by_default; } bool is_disabled_by_default() const { return this->flags_.disabled_by_default; }
void set_disabled_by_default(bool disabled_by_default) { this->flags_.disabled_by_default = disabled_by_default; }
// Get/set the entity category. // Get the entity category.
EntityCategory get_entity_category() const { return static_cast<EntityCategory>(this->flags_.entity_category); } EntityCategory get_entity_category() const { return static_cast<EntityCategory>(this->flags_.entity_category); }
void set_entity_category(EntityCategory entity_category) {
this->flags_.entity_category = static_cast<uint8_t>(entity_category);
}
// Get this entity's device class into a stack buffer. // Get this entity's device class into a stack buffer.
// On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused). // On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused).
@@ -164,14 +168,13 @@ class EntityBase {
#endif #endif
#ifdef USE_DEVICES #ifdef USE_DEVICES
// Get/set this entity's device id // Get this entity's device id
uint32_t get_device_id() const { uint32_t get_device_id() const {
if (this->device_ == nullptr) { if (this->device_ == nullptr) {
return 0; // No device set, return 0 return 0; // No device set, return 0
} }
return this->device_->get_device_id(); return this->device_->get_device_id();
} }
void set_device(Device *device) { this->device_ = device; }
// Get the device this entity belongs to (nullptr if main device) // Get the device this entity belongs to (nullptr if main device)
Device *get_device() const { return this->device_; } Device *get_device() const { return this->device_; }
#endif #endif
@@ -228,8 +231,14 @@ class EntityBase {
friend void ::setup(); friend void ::setup();
friend void ::original_setup(); friend void ::original_setup();
/// Combined entity setup from codegen: set name, object_id hash, and entity string indices. /// Combined entity setup from codegen: set name, object_id hash, entity string indices, and flags.
void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed); /// Bit layout of entity_fields is defined by the ENTITY_FIELD_*_SHIFT constants above.
void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields);
#ifdef USE_DEVICES
// Codegen-only setter — only accessible from setup() via friend declaration.
void set_device_(Device *device) { this->device_ = device; }
#endif
/// Non-template helper for make_entity_preference() to avoid code bloat. /// Non-template helper for make_entity_preference() to avoid code bloat.
/// When preference hash algorithm changes, migration logic goes here. /// When preference hash algorithm changes, migration logic goes here.
+73 -11
View File
@@ -34,11 +34,19 @@ _KEY_ICON_IDX = "_entity_icon_idx"
_KEY_ENTITY_NAME = "_entity_name" _KEY_ENTITY_NAME = "_entity_name"
_KEY_OBJECT_ID_HASH = "_entity_object_id_hash" _KEY_OBJECT_ID_HASH = "_entity_object_id_hash"
# Bit layout for entity_strings_packed in configure_entity_() — must match C++ in entity_base.h: # Bit layout for entity_fields in configure_entity_().
# [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits) # Keep in sync with ENTITY_FIELD_*_SHIFT constants in esphome/core/entity_base.h
_DC_SHIFT = 0 _DC_SHIFT = 0
_UOM_SHIFT = 8 _UOM_SHIFT = 8
_ICON_SHIFT = 16 _ICON_SHIFT = 16
_INTERNAL_SHIFT = 24
_DISABLED_BY_DEFAULT_SHIFT = 25
_ENTITY_CATEGORY_SHIFT = 26
# Private config keys for storing flags
_KEY_INTERNAL = "_entity_internal"
_KEY_DISABLED_BY_DEFAULT = "_entity_disabled_by_default"
_KEY_ENTITY_CATEGORY = "_entity_category"
# Maximum unique strings per category (8-bit index, 0 = not set) # Maximum unique strings per category (8-bit index, 0 = not set)
_MAX_DEVICE_CLASSES = 0xFF # 255 _MAX_DEVICE_CLASSES = 0xFF # 255
@@ -220,8 +228,39 @@ def setup_unit_of_measurement(config: ConfigType) -> None:
config[_KEY_UOM_IDX] = idx config[_KEY_UOM_IDX] = idx
def _sanitize_comment(text: str) -> str:
r"""Sanitize a string for safe inclusion in a C++ // line comment.
Dangerous characters:
- \n, \r: break out of line comment, next line becomes code
- \: at end of line, splices next line into comment (eats real code)
"""
return text.replace("\\", "/").replace("\n", " ").replace("\r", "")
def _describe_packed_flags(config: ConfigType, entity_category: int) -> str:
"""Build a human-readable description of packed entity flags for C++ comments."""
parts: list[str] = []
if config.get(_KEY_INTERNAL):
parts.append("internal")
if config.get(_KEY_DISABLED_BY_DEFAULT):
parts.append("disabled_by_default")
entity_cat_keys = list(cv.ENTITY_CATEGORIES)
if entity_category < len(entity_cat_keys) and (
cat_name := entity_cat_keys[entity_category]
):
parts.append(f"category:{cat_name}")
if dc := config.get(CONF_DEVICE_CLASS):
parts.append(f"dc:{_sanitize_comment(dc)}")
if uom := config.get(CONF_UNIT_OF_MEASUREMENT):
parts.append(f"uom:{_sanitize_comment(uom)}")
if icon := config.get(CONF_ICON):
parts.append(f"icon:{_sanitize_comment(icon)}")
return ", ".join(parts)
def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
"""Emit a single configure_entity_() call with name, hash, and packed string indices. """Emit a single configure_entity_() call with name, hash, packed string indices, and flags.
Call this at the end of each component's setup function, after Call this at the end of each component's setup function, after
setup_entity() and any register_device_class/register_unit_of_measurement calls. setup_entity() and any register_device_class/register_unit_of_measurement calls.
@@ -231,8 +270,24 @@ def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
dc_idx = config.get(_KEY_DC_IDX, 0) dc_idx = config.get(_KEY_DC_IDX, 0)
uom_idx = config.get(_KEY_UOM_IDX, 0) uom_idx = config.get(_KEY_UOM_IDX, 0)
icon_idx = config.get(_KEY_ICON_IDX, 0) icon_idx = config.get(_KEY_ICON_IDX, 0)
packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT) internal = config.get(_KEY_INTERNAL, 0)
add(var.configure_entity_(entity_name, object_id_hash, packed)) disabled_by_default = config.get(_KEY_DISABLED_BY_DEFAULT, 0)
entity_category = config.get(_KEY_ENTITY_CATEGORY, 0)
packed = (
(dc_idx << _DC_SHIFT)
| (uom_idx << _UOM_SHIFT)
| (icon_idx << _ICON_SHIFT)
| (internal << _INTERNAL_SHIFT)
| (disabled_by_default << _DISABLED_BY_DEFAULT_SHIFT)
| (entity_category << _ENTITY_CATEGORY_SHIFT)
)
# Build inline comment describing the packed flags for readability
comment = _describe_packed_flags(config, entity_category)
expr = var.configure_entity_(entity_name, object_id_hash, packed)
if comment:
add(RawStatement(f"{expr}; // {comment}"))
else:
add(expr)
def get_base_entity_object_id( def get_base_entity_object_id(
@@ -332,7 +387,7 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) ->
# Get device info if configured # Get device info if configured
if device_id_obj := config.get(CONF_DEVICE_ID): if device_id_obj := config.get(CONF_DEVICE_ID):
device: MockObj = await get_variable(device_id_obj) device: MockObj = await get_variable(device_id_obj)
add(var.set_device(device)) add(var.set_device_(device))
# Pre-compute entity name and object_id hash for configure_entity_() # Pre-compute entity name and object_id hash for configure_entity_()
# which is emitted later by finalize_entity_strings(). # which is emitted later by finalize_entity_strings().
@@ -343,18 +398,25 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) ->
object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0 object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0
config[_KEY_ENTITY_NAME] = entity_name config[_KEY_ENTITY_NAME] = entity_name
config[_KEY_OBJECT_ID_HASH] = object_id_hash config[_KEY_OBJECT_ID_HASH] = object_id_hash
# Only set disabled_by_default if True (default is False) # Store flags for packing into configure_entity_()
if config[CONF_DISABLED_BY_DEFAULT]: config[_KEY_DISABLED_BY_DEFAULT] = int(config[CONF_DISABLED_BY_DEFAULT])
add(var.set_disabled_by_default(True))
if CONF_INTERNAL in config: if CONF_INTERNAL in config:
add(var.set_internal(config[CONF_INTERNAL])) config[_KEY_INTERNAL] = int(config[CONF_INTERNAL])
icon_idx = 0 icon_idx = 0
if CONF_ICON in config: if CONF_ICON in config:
# Add USE_ENTITY_ICON define when icons are used # Add USE_ENTITY_ICON define when icons are used
cg.add_define("USE_ENTITY_ICON") cg.add_define("USE_ENTITY_ICON")
icon_idx = register_icon(config[CONF_ICON]) icon_idx = register_icon(config[CONF_ICON])
if CONF_ENTITY_CATEGORY in config: if CONF_ENTITY_CATEGORY in config:
add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) # Derive integer value from key position in cv.ENTITY_CATEGORIES
# (must match C++ EntityCategory enum in entity_base.h)
entity_cat_str = str(config[CONF_ENTITY_CATEGORY])
entity_cat_keys = list(cv.ENTITY_CATEGORIES)
config[_KEY_ENTITY_CATEGORY] = (
entity_cat_keys.index(entity_cat_str)
if entity_cat_str in entity_cat_keys
else 0
)
# Store icon index for finalize_entity_strings # Store icon index for finalize_entity_strings
config[_KEY_ICON_IDX] = icon_idx config[_KEY_ICON_IDX] = icon_idx
@@ -45,8 +45,9 @@ def test_binary_sensor_config_value_internal_set(generate_main):
) )
# Then # Then
assert "bs_1->set_internal(true);" in main_cpp # internal flag is now packed into configure_entity_() third argument (bit 24)
assert "bs_2->set_internal(false);" in main_cpp assert "bs_1->configure_entity_(" in main_cpp
assert "bs_2->configure_entity_(" in main_cpp
def test_binary_sensor_config_value_use_raw_set(generate_main): def test_binary_sensor_config_value_use_raw_set(generate_main):
+4 -2
View File
@@ -40,5 +40,7 @@ def test_button_config_value_internal_set(generate_main):
main_cpp = generate_main("tests/component_tests/button/test_button.yaml") main_cpp = generate_main("tests/component_tests/button/test_button.yaml")
# Then # Then
assert "wol_1->set_internal(true);" in main_cpp # internal flag is packed into configure_entity_() third argument (bit 24)
assert "wol_2->set_internal(false);" in main_cpp # wol_1 has internal: true → bit 24 set → packed value 16777216
assert "wol_1->configure_entity_(" in main_cpp
assert "wol_2->configure_entity_(" in main_cpp
+13 -2
View File
@@ -1,5 +1,15 @@
"""Tests for the sensor component.""" """Tests for the sensor component."""
import re
def _extract_packed_value(main_cpp, var_name):
"""Extract the third (packed) argument from a configure_entity_ call."""
pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)"
match = re.search(pattern, main_cpp)
assert match, f"configure_entity_ call not found for {var_name}"
return int(match.group(1))
def test_sensor_device_class_set(generate_main): def test_sensor_device_class_set(generate_main):
""" """
@@ -10,5 +20,6 @@ def test_sensor_device_class_set(generate_main):
# When # When
main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml") main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml")
# Then # Then: device_class: voltage means packed value must be non-zero
assert "s_1->configure_entity_(" in main_cpp packed = _extract_packed_value(main_cpp, "s_1")
assert packed != 0
+3 -2
View File
@@ -38,8 +38,9 @@ def test_text_config_value_internal_set(generate_main):
main_cpp = generate_main("tests/component_tests/text/test_text.yaml") main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
# Then # Then
assert "it_2->set_internal(false);" in main_cpp # internal flag is now packed into configure_entity_() third argument (bit 24)
assert "it_3->set_internal(true);" in main_cpp assert "it_2->configure_entity_(" in main_cpp
assert "it_3->configure_entity_(" in main_cpp
def test_text_config_value_mode_set(generate_main): def test_text_config_value_mode_set(generate_main):
@@ -1,5 +1,15 @@
"""Tests for the text sensor component.""" """Tests for the text sensor component."""
import re
def _extract_packed_value(main_cpp, var_name):
"""Extract the third (packed) argument from a configure_entity_ call."""
pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)"
match = re.search(pattern, main_cpp)
assert match, f"configure_entity_ call not found for {var_name}"
return int(match.group(1))
def test_text_sensor_is_setup(generate_main): def test_text_sensor_is_setup(generate_main):
""" """
@@ -40,8 +50,9 @@ def test_text_sensor_config_value_internal_set(generate_main):
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
# Then # Then
assert "ts_2->set_internal(true);" in main_cpp # internal flag is now packed into configure_entity_() third argument (bit 24)
assert "ts_3->set_internal(false);" in main_cpp assert "ts_2->configure_entity_(" in main_cpp
assert "ts_3->configure_entity_(" in main_cpp
def test_text_sensor_device_class_set(generate_main): def test_text_sensor_device_class_set(generate_main):
@@ -53,6 +64,9 @@ def test_text_sensor_device_class_set(generate_main):
# When # When
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
# Then # Then: ts_2 has device_class: timestamp, ts_3 has device_class: date
assert "ts_2->configure_entity_(" in main_cpp # so their packed values must be non-zero
assert "ts_3->configure_entity_(" in main_cpp packed_ts_2 = _extract_packed_value(main_cpp, "ts_2")
assert packed_ts_2 != 0
packed_ts_3 = _extract_packed_value(main_cpp, "ts_3")
assert packed_ts_3 != 0
+216 -26
View File
@@ -9,6 +9,7 @@ import pytest
from esphome.config_validation import Invalid from esphome.config_validation import Invalid
from esphome.const import ( from esphome.const import (
CONF_DEVICE_CLASS,
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_DISABLED_BY_DEFAULT, CONF_DISABLED_BY_DEFAULT,
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
@@ -16,16 +17,26 @@ from esphome.const import (
CONF_ID, CONF_ID,
CONF_INTERNAL, CONF_INTERNAL,
CONF_NAME, CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
) )
from esphome.core import CORE, ID, entity_helpers from esphome.core import CORE, ID, entity_helpers
from esphome.core.entity_helpers import ( from esphome.core.entity_helpers import (
_DC_SHIFT,
_DISABLED_BY_DEFAULT_SHIFT,
_ENTITY_CATEGORY_SHIFT,
_ICON_SHIFT,
_INTERNAL_SHIFT,
_UOM_SHIFT,
_register_string, _register_string,
_setup_entity_impl, _setup_entity_impl,
entity_duplicate_validator, entity_duplicate_validator,
finalize_entity_strings,
get_base_entity_object_id, get_base_entity_object_id,
register_device_class, register_device_class,
register_icon, register_icon,
setup_device_class,
setup_entity, setup_entity,
setup_unit_of_measurement,
) )
from esphome.cpp_generator import MockObj from esphome.cpp_generator import MockObj
from esphome.helpers import sanitize, snake_case from esphome.helpers import sanitize, snake_case
@@ -309,8 +320,6 @@ def extract_object_id_from_expressions(expressions: list[str]) -> str | None:
async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> None: async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> None:
"""Test setup_entity with unique names.""" """Test setup_entity with unique names."""
setup_test_environment # noqa: F841 - fixture initializes CORE state
# Create mock entities # Create mock entities
var1 = MockObj("sensor1") var1 = MockObj("sensor1")
var2 = MockObj("sensor2") var2 = MockObj("sensor2")
@@ -344,8 +353,6 @@ async def test_setup_entity_different_platforms(
) -> None: ) -> None:
"""Test that same name on different platforms doesn't conflict.""" """Test that same name on different platforms doesn't conflict."""
setup_test_environment # noqa: F841 - fixture initializes CORE state
# Create mock entities # Create mock entities
sensor = MockObj("sensor1") sensor = MockObj("sensor1")
binary_sensor = MockObj("binary_sensor1") binary_sensor = MockObj("binary_sensor1")
@@ -392,7 +399,6 @@ async def test_setup_entity_with_devices(
setup_test_environment: list[str], mock_get_variable: dict[ID, MockObj] setup_test_environment: list[str], mock_get_variable: dict[ID, MockObj]
) -> None: ) -> None:
"""Test that same name on different devices doesn't conflict.""" """Test that same name on different devices doesn't conflict."""
setup_test_environment # noqa: F841 - fixture initializes CORE state
# Create mock devices # Create mock devices
device1_id = ID("device1", type="Device") device1_id = ID("device1", type="Device")
@@ -433,8 +439,6 @@ async def test_setup_entity_with_devices(
async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> None: async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> None:
"""Test setup_entity with empty entity name.""" """Test setup_entity with empty entity name."""
setup_test_environment # noqa: F841 - fixture initializes CORE state
var = MockObj("sensor1") var = MockObj("sensor1")
config = { config = {
@@ -455,8 +459,6 @@ async def test_setup_entity_special_characters(
) -> None: ) -> None:
"""Test setup_entity with names containing special characters.""" """Test setup_entity with names containing special characters."""
setup_test_environment # noqa: F841 - fixture initializes CORE state
var = MockObj("sensor1") var = MockObj("sensor1")
config = { config = {
@@ -475,8 +477,6 @@ async def test_setup_entity_special_characters(
async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None: async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None:
"""Test setup_entity sets icon correctly.""" """Test setup_entity sets icon correctly."""
setup_test_environment # noqa: F841 - fixture initializes CORE state
var = MockObj("sensor1") var = MockObj("sensor1")
config = { config = {
@@ -497,8 +497,6 @@ async def test_setup_entity_disabled_by_default(
) -> None: ) -> None:
"""Test setup_entity sets disabled_by_default correctly.""" """Test setup_entity sets disabled_by_default correctly."""
added_expressions = setup_test_environment
var = MockObj("sensor1") var = MockObj("sensor1")
config = { config = {
@@ -508,10 +506,8 @@ async def test_setup_entity_disabled_by_default(
await _setup_entity_impl(var, config, "sensor") await _setup_entity_impl(var, config, "sensor")
# Check disabled_by_default was set # disabled_by_default is now packed into config for configure_entity_()
assert any( assert config.get("_entity_disabled_by_default") == 1
"sensor1.set_disabled_by_default(true)" in expr for expr in added_expressions
)
def test_entity_duplicate_validator() -> None: def test_entity_duplicate_validator() -> None:
@@ -796,8 +792,8 @@ async def test_setup_entity_empty_name_with_device(
entity_helpers.get_variable = original_get_variable entity_helpers.get_variable = original_get_variable
# Check that set_device was called # Check that set_device_ was called (separate protected call, accessible via friend)
assert any("sensor1.set_device" in expr for expr in added_expressions) assert any("sensor1.set_device_" in expr for expr in added_expressions)
# For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime
assert config.get("_entity_name") == "" assert config.get("_entity_name") == ""
@@ -813,7 +809,6 @@ async def test_setup_entity_empty_name_with_mac_suffix(
For empty-name entities, Python passes 0 and C++ calculates the hash For empty-name entities, Python passes 0 and C++ calculates the hash
at runtime from friendly_name (bug-for-bug compatibility). at runtime from friendly_name (bug-for-bug compatibility).
""" """
setup_test_environment # noqa: F841 - fixture initializes CORE state
# Set up CORE.config with name_add_mac_suffix enabled # Set up CORE.config with name_add_mac_suffix enabled
CORE.config = {"name_add_mac_suffix": True} CORE.config = {"name_add_mac_suffix": True}
@@ -844,7 +839,6 @@ async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name(
at runtime. In this case C++ will hash the empty friendly_name at runtime. In this case C++ will hash the empty friendly_name
(bug-for-bug compatibility). (bug-for-bug compatibility).
""" """
setup_test_environment # noqa: F841 - fixture initializes CORE state
# Set up CORE.config with name_add_mac_suffix enabled # Set up CORE.config with name_add_mac_suffix enabled
CORE.config = {"name_add_mac_suffix": True} CORE.config = {"name_add_mac_suffix": True}
@@ -874,7 +868,6 @@ async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name(
For empty-name entities, Python passes 0 and C++ calculates the hash For empty-name entities, Python passes 0 and C++ calculates the hash
at runtime from the device name. at runtime from the device name.
""" """
setup_test_environment # noqa: F841 - fixture initializes CORE state
# No MAC suffix (either not set or False) # No MAC suffix (either not set or False)
CORE.config = {} CORE.config = {}
@@ -942,7 +935,7 @@ def test_register_device_class_max_length() -> None:
async def test_setup_entity_with_entity_category( async def test_setup_entity_with_entity_category(
setup_test_environment: list[str], setup_test_environment: list[str],
) -> None: ) -> None:
"""Test setup_entity sets entity_category correctly.""" """Test entity_category is packed correctly through the full setup flow."""
added_expressions = setup_test_environment added_expressions = setup_test_environment
var = MockObj("sensor1") var = MockObj("sensor1")
config = { config = {
@@ -951,9 +944,9 @@ async def test_setup_entity_with_entity_category(
CONF_ENTITY_CATEGORY: "diagnostic", CONF_ENTITY_CATEGORY: "diagnostic",
} }
await _setup_entity_impl(var, config, "sensor") await _setup_entity_impl(var, config, "sensor")
assert any( finalize_entity_strings(var, config)
'set_entity_category("diagnostic")' in expr for expr in added_expressions packed = _extract_packed_value(added_expressions)
) assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 2
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -1002,3 +995,200 @@ async def test_setup_entity_decorator_mode(setup_test_environment: list[str]) ->
assert body_called assert body_called
object_id = extract_object_id_from_expressions(added_expressions) object_id = extract_object_id_from_expressions(added_expressions)
assert object_id == "temperature" assert object_id == "temperature"
# Tests for finalize_entity_strings packing
def _extract_packed_value(expressions: list[str]) -> int:
"""Extract the third argument (packed value) from a configure_entity_() call."""
for expr in expressions:
if "configure_entity_" in expr:
# Match the last integer argument before the closing ");"
match = re.search(r",\s*(\d+)\s*\)", expr)
if match:
return int(match.group(1))
raise AssertionError("No configure_entity_ call found")
@pytest.mark.asyncio
async def test_finalize_no_flags(setup_test_environment: list[str]) -> None:
"""Test entity with no special flags — packed value is 0, no comment."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
config = {
CONF_NAME: "Test",
CONF_DISABLED_BY_DEFAULT: False,
}
await _setup_entity_impl(var, config, "sensor")
finalize_entity_strings(var, config)
packed = _extract_packed_value(added_expressions)
assert packed == 0
assert "//" not in added_expressions[0]
@pytest.mark.asyncio
async def test_finalize_internal(setup_test_environment: list[str]) -> None:
"""Test entity with internal=True packs the internal bit."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
config = {
CONF_NAME: "Test",
CONF_DISABLED_BY_DEFAULT: False,
CONF_INTERNAL: True,
}
await _setup_entity_impl(var, config, "sensor")
finalize_entity_strings(var, config)
packed = _extract_packed_value(added_expressions)
assert packed & (1 << _INTERNAL_SHIFT) != 0
assert packed == (1 << _INTERNAL_SHIFT)
@pytest.mark.asyncio
async def test_finalize_disabled_by_default(
setup_test_environment: list[str],
) -> None:
"""Test entity with disabled_by_default=True packs the bit."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
config = {
CONF_NAME: "Test",
CONF_DISABLED_BY_DEFAULT: True,
}
await _setup_entity_impl(var, config, "sensor")
finalize_entity_strings(var, config)
packed = _extract_packed_value(added_expressions)
assert packed & (1 << _DISABLED_BY_DEFAULT_SHIFT) != 0
assert packed == (1 << _DISABLED_BY_DEFAULT_SHIFT)
@pytest.mark.asyncio
async def test_finalize_entity_category(
setup_test_environment: list[str],
) -> None:
"""Test entity_category values (diagnostic=2, config=1) are packed."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
# Test diagnostic (value 2)
config = {
CONF_NAME: "Test",
CONF_DISABLED_BY_DEFAULT: False,
CONF_ENTITY_CATEGORY: "diagnostic",
}
await _setup_entity_impl(var, config, "sensor")
finalize_entity_strings(var, config)
packed = _extract_packed_value(added_expressions)
assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 2
# Test config (value 1)
added_expressions.clear()
config2 = {
CONF_NAME: "Test2",
CONF_DISABLED_BY_DEFAULT: False,
CONF_ENTITY_CATEGORY: "config",
}
await _setup_entity_impl(var, config2, "sensor")
finalize_entity_strings(var, config2)
packed = _extract_packed_value(added_expressions)
assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 1
@pytest.mark.asyncio
async def test_finalize_string_indices(
setup_test_environment: list[str],
) -> None:
"""Test device_class, unit_of_measurement, and icon are packed as indices."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
config = {
CONF_NAME: "Test",
CONF_DISABLED_BY_DEFAULT: False,
CONF_DEVICE_CLASS: "temperature",
CONF_UNIT_OF_MEASUREMENT: "°C",
CONF_ICON: "mdi:thermometer",
}
await _setup_entity_impl(var, config, "sensor")
setup_device_class(config)
setup_unit_of_measurement(config)
finalize_entity_strings(var, config)
packed = _extract_packed_value(added_expressions)
# All three string indices should be non-zero
assert (packed >> _DC_SHIFT) & 0xFF != 0
assert (packed >> _UOM_SHIFT) & 0xFF != 0
assert (packed >> _ICON_SHIFT) & 0xFF != 0
# No flags set
assert (packed >> _INTERNAL_SHIFT) & 1 == 0
assert (packed >> _DISABLED_BY_DEFAULT_SHIFT) & 1 == 0
assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 0
@pytest.mark.asyncio
async def test_finalize_all_fields(
setup_test_environment: list[str],
) -> None:
"""Test all fields set: flags, string indices, and comment."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
config = {
CONF_NAME: "Test",
CONF_DISABLED_BY_DEFAULT: True,
CONF_INTERNAL: True,
CONF_ENTITY_CATEGORY: "diagnostic",
CONF_DEVICE_CLASS: "temperature",
CONF_UNIT_OF_MEASUREMENT: "°C",
CONF_ICON: "mdi:thermometer",
}
await _setup_entity_impl(var, config, "sensor")
setup_device_class(config)
setup_unit_of_measurement(config)
finalize_entity_strings(var, config)
packed = _extract_packed_value(added_expressions)
# Verify flags
assert (packed >> _INTERNAL_SHIFT) & 1 == 1
assert (packed >> _DISABLED_BY_DEFAULT_SHIFT) & 1 == 1
assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 2
# Verify string indices are non-zero
assert (packed >> _DC_SHIFT) & 0xFF != 0
assert (packed >> _UOM_SHIFT) & 0xFF != 0
assert (packed >> _ICON_SHIFT) & 0xFF != 0
# Verify comment contains all flags with actual string values
comment_line = added_expressions[0]
assert (
"// internal, disabled_by_default, category:diagnostic,"
" dc:temperature, uom:°C, icon:mdi:thermometer" in comment_line
)
@pytest.mark.asyncio
async def test_finalize_comment_sanitization(
setup_test_environment: list[str],
) -> None:
"""Test that user strings in comments are sanitized against injection."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
config = {
CONF_NAME: "Test",
CONF_DISABLED_BY_DEFAULT: False,
# Backslash at end would cause line splice eating next code line
CONF_ICON: "mdi:evil\\",
}
await _setup_entity_impl(var, config, "sensor")
finalize_entity_strings(var, config)
comment_line = added_expressions[0]
# Backslash must be replaced to prevent line splice
assert "\\" not in comment_line
assert "mdi:evil/" in comment_line
added_expressions.clear()
config2 = {
CONF_NAME: "Test2",
CONF_DISABLED_BY_DEFAULT: False,
CONF_ICON: "mdi:evil\nINJECTED_CODE();",
}
await _setup_entity_impl(var, config2, "sensor")
finalize_entity_strings(var, config2)
comment_line = added_expressions[0]
# Newline must be replaced to prevent breaking out of comment
assert "\n" not in comment_line
assert "INJECTED_CODE" in comment_line # still visible but safe in comment