diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 412964d0f8..ee7fe5ce75 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -619,7 +619,7 @@ void ATM90E32Component::run_gain_calibrations() { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs, phase_labels[phase]); } else { - uint32_t new_voltage_gain = static_cast((ref_voltage / measured_voltage) * current_voltage_gain); + uint32_t new_voltage_gain = static_cast((ref_voltage / measured_voltage) * current_voltage_gain); if (new_voltage_gain == 0) { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs, 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, phase_labels[phase]); } else { - uint32_t new_current_gain = static_cast((ref_current / measured_current) * current_current_gain); + uint32_t new_current_gain = static_cast((ref_current / measured_current) * current_current_gain); if (new_current_gain == 0) { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs, phase_labels[phase]); diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h index 6258795b02..59b0af93ad 100644 --- a/esphome/components/bedjet/bedjet_hub.h +++ b/esphome/components/bedjet/bedjet_hub.h @@ -164,10 +164,10 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo std::unique_ptr codec_; bool discover_characteristics_(); - uint16_t char_handle_cmd_; - uint16_t char_handle_name_; - uint16_t char_handle_status_; - uint16_t config_descr_status_; + uint16_t char_handle_cmd_{0}; + uint16_t char_handle_name_{0}; + uint16_t char_handle_status_{0}; + uint16_t config_descr_status_{0}; uint8_t write_notify_config_descriptor_(bool enable); }; diff --git a/esphome/components/current_based/current_based_cover.h b/esphome/components/current_based/current_based_cover.h index 76bd85cdf7..40b39517e4 100644 --- a/esphome/components/current_based/current_based_cover.h +++ b/esphome/components/current_based/current_based_cover.h @@ -67,21 +67,21 @@ class CurrentBasedCover : public cover::Cover, public Component { sensor::Sensor *open_sensor_{nullptr}; Trigger<> open_trigger_; - float open_moving_current_threshold_; + float open_moving_current_threshold_{0.0f}; float open_obstacle_current_threshold_{FLT_MAX}; - uint32_t open_duration_; + uint32_t open_duration_{0}; sensor::Sensor *close_sensor_{nullptr}; Trigger<> close_trigger_; - float close_moving_current_threshold_; + float close_moving_current_threshold_{0.0f}; float close_obstacle_current_threshold_{FLT_MAX}; - uint32_t close_duration_; + uint32_t close_duration_{0}; uint32_t max_duration_{UINT32_MAX}; bool malfunction_detection_{true}; Trigger<> malfunction_trigger_; - uint32_t start_sensing_delay_; - float obstacle_rollback_; + uint32_t start_sensing_delay_{0}; + float obstacle_rollback_{0.0f}; Trigger<> *prev_command_trigger_{nullptr}; uint32_t last_recompute_time_{0}; diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index cbf2b85506..8233e809a1 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -91,7 +91,7 @@ class DateCall { DateEntity *parent_; - optional year_; + optional year_; optional month_; optional day_; }; diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 1998b815f3..14713d51a1 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -145,7 +145,7 @@ class DeepSleepComponent : public Component { #endif // USE_BK72XX #ifdef USE_ESP32 - InternalGPIOPin *wakeup_pin_; + InternalGPIOPin *wakeup_pin_{nullptr}; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp index 1358121c1b..4371075fa9 100644 --- a/esphome/components/es7210/es7210.cpp +++ b/esphome/components/es7210/es7210.cpp @@ -172,7 +172,7 @@ uint8_t ES7210::es7210_gain_reg_value_(float mic_gain) { // reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB mic_gain += 0.5; if (mic_gain <= 33.0) { - return (uint8_t) mic_gain / 3; + return (uint8_t) (mic_gain / 3); } if (mic_gain < 36.0) { return 12; diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index f292cf8722..ecc53e197f 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -7,6 +7,7 @@ #ifdef USE_ESP32 +#include #include #include #include @@ -38,21 +39,16 @@ void BLEServer::loop() { case RUNNING: { // Start all services that are pending to start if (!this->services_to_start_.empty()) { - uint16_t index_to_remove = 0; - // Iterate over the services to start - for (unsigned i = 0; i < this->services_to_start_.size(); i++) { - BLEService *service = this->services_to_start_[i]; + for (auto &service : this->services_to_start_) { if (service->is_created()) { 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 - if (index_to_remove > 0) { - this->services_to_start_.erase(this->services_to_start_.begin(), - this->services_to_start_.begin() + index_to_remove - 1); - } + // Remove services that have been started + this->services_to_start_.erase( + std::remove_if(this->services_to_start_.begin(), this->services_to_start_.end(), + [](BLEService *service) { return service->is_starting() || service->is_running(); }), + this->services_to_start_.end()); } break; } diff --git a/esphome/components/espnow/automation.h b/esphome/components/espnow/automation.h index 0b26681400..0fbb14e388 100644 --- a/esphome/components/espnow/automation.h +++ b/esphome/components/espnow/automation.h @@ -138,7 +138,7 @@ class OnReceiveTrigger : public Trigger, public ESPNowUnknownPeerHandler { @@ -167,7 +167,7 @@ class OnBroadcastedTrigger : public Triggerlast_move_ < this->move_interval_) return; - if (direction_) { + const auto num_leds = static_cast(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_++; - if (this->at_led_ == it.size() - this->scan_width_) + if (this->at_led_ >= max_pos) this->direction_ = false; } else { - this->at_led_--; + if (this->at_led_ > 0) + this->at_led_--; if (this->at_led_ == 0) this->direction_ = true; } diff --git a/esphome/components/max6956/max6956.h b/esphome/components/max6956/max6956.h index 0c609b0b43..31f97c11f8 100644 --- a/esphome/components/max6956/max6956.h +++ b/esphome/components/max6956/max6956.h @@ -63,8 +63,8 @@ class MAX6956 : public Component, public i2c::I2CDevice { bool read_reg_(uint8_t reg, uint8_t *value); // write a value to a given register bool write_reg_(uint8_t reg, uint8_t value); - max6956::MAX6956CURRENTMODE brightness_mode_; - uint8_t global_brightness_; + max6956::MAX6956CURRENTMODE brightness_mode_{}; + uint8_t global_brightness_{0}; private: int8_t prev_bright_[28] = {0}; diff --git a/esphome/components/ms8607/ms8607.h b/esphome/components/ms8607/ms8607.h index ceb3dd22c8..2888b6cdd2 100644 --- a/esphome/components/ms8607/ms8607.h +++ b/esphome/components/ms8607/ms8607.h @@ -67,9 +67,9 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { /// use raw temperature & pressure to calculate & publish values void calculate_values_(uint32_t raw_temperature, uint32_t raw_pressure); - sensor::Sensor *temperature_sensor_; - sensor::Sensor *pressure_sensor_; - sensor::Sensor *humidity_sensor_; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; /** 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 */ - MS8607HumidityDevice *humidity_device_; + MS8607HumidityDevice *humidity_device_{nullptr}; /// This device's pressure & temperature calibration values, read from PROM struct CalibrationValues { diff --git a/esphome/components/noblex/noblex.cpp b/esphome/components/noblex/noblex.cpp index f1e76eabf2..e7e421d177 100644 --- a/esphome/components/noblex/noblex.cpp +++ b/esphome/components/noblex/noblex.cpp @@ -118,15 +118,15 @@ void NoblexClimate::transmit_state() { data->mark(NOBLEX_HEADER_MARK); data->space(NOBLEX_HEADER_SPACE); // Data (sent remote_state from the MSB to the LSB) - for (uint8_t i : remote_state) { - for (int8_t j = 7; j >= 0; j--) { - if ((i == 4) & (j == 4)) { + for (int byte_idx = 0; byte_idx < 8; byte_idx++) { + for (int8_t bit_idx = 7; bit_idx >= 0; bit_idx--) { + if ((byte_idx == 4) && (bit_idx == 4)) { // Header intermediate data->mark(NOBLEX_BIT_MARK); data->space(NOBLEX_GAP); // gap en bit 36 } else { 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); } } @@ -145,76 +145,71 @@ void NoblexClimate::transmit_state() { // Handle received IR Buffer bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { - uint8_t remote_state[8] = {0}; - uint8_t crc = 0, crc_calculated = 0; - - if (!receiving_) { - // Validate header - if (data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) { - ESP_LOGV(TAG, "Header"); - 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 + if (data.peek_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) { + // First part: header + first 36 bits, followed by 20ms gap + data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE); + ESP_LOGV(TAG, "Header"); + this->receiving_ = false; + memset(this->remote_state_, 0, sizeof(this->remote_state_)); + for (int i = 0; i < 5; i++) { for (int j = 7; j >= 0; j--) { - if ((i == 4) & (j >= 4)) { - // nothing + if ((i == 4) && (j == 4)) { + 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)) { - remote_state[i] |= 1 << j; + this->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]); + ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]); } + return false; + } - // Read crc - for (int i = 3; i >= 0; i--) { - if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { - crc |= 1 << i; + // Second part: remaining 28 bits + 4-bit CRC + footer + if (!this->receiving_) { + return false; + } + 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)) { - ESP_LOGVV(TAG, "Bit %d CRC fail", i); + ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); return false; } } - ESP_LOGV(TAG, "CRC %02X", crc); - - // Validate footer - if (!data.expect_mark(NOBLEX_BIT_MARK)) { - ESP_LOGV(TAG, "Footer fail"); - return false; - } - receiving_ = false; + ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]); } - 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(uint8_t(crc_calculated & 0x0F)) >> 4; ESP_LOGVV(TAG, "CRC calc %02X", crc_calculated); @@ -224,11 +219,12 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { return false; } - ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", remote_state[0], remote_state[1], - remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], remote_state[7]); + ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", this->remote_state_[0], + 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; - if ((remote_state[0] & NOBLEX_POWER) == NOBLEX_POWER) { + if ((this->remote_state_[0] & NOBLEX_POWER) == NOBLEX_POWER) { powered_on = true; this->powered_on_assumed = powered_on; } else { @@ -241,7 +237,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { // Set received mode 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); switch (mode) { case IRNoblexMode::IR_NOBLEX_MODE_AUTO: @@ -263,7 +259,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { } // Set received temp - uint8_t temp = remote_state[1]; + uint8_t temp = this->remote_state_[1]; ESP_LOGVV(TAG, "Temperature Raw: %02X", temp); temp = 0x0F & reverse_bits(temp); @@ -272,7 +268,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { this->target_temperature = temp; // 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); switch (fan) { case IRNoblexFan::IR_NOBLEX_FAN_HIGH: @@ -291,7 +287,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { } // Set received swing status - if (remote_state[0] & 0x02) { + if (this->remote_state_[0] & 0x02) { ESP_LOGV(TAG, "Swing vertical"); this->swing_mode = climate::CLIMATE_SWING_VERTICAL; } else { @@ -299,8 +295,6 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { this->swing_mode = climate::CLIMATE_SWING_OFF; } - for (uint8_t &i : remote_state) - i = 0; this->publish_state(); return true; } // end on_receive() diff --git a/esphome/components/noblex/noblex.h b/esphome/components/noblex/noblex.h index 57990db005..3d52a1a538 100644 --- a/esphome/components/noblex/noblex.h +++ b/esphome/components/noblex/noblex.h @@ -41,7 +41,8 @@ class NoblexClimate : public climate_ir::ClimateIR { /// Handle received IR Buffer. bool on_receive(remote_base::RemoteReceiveData data) override; bool send_swing_cmd_{false}; - bool receiving_ = false; + bool receiving_{false}; + uint8_t remote_state_[8]{}; }; } // namespace noblex diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index aee52ea170..6b4ebfe24b 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -96,7 +96,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, bool inverted_{false}; bool non_blocking_{false}; #endif - uint8_t carrier_duty_percent_; + uint8_t carrier_duty_percent_{50}; Trigger<> transmit_trigger_; Trigger<> complete_trigger_; diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index dc0d3c315a..fdb49fb3ef 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -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 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"); this->mark_failed(); return; diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index e3bf931b41..a9d4da86b8 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -104,12 +104,12 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri char serial_number_[17] = "UNKNOWN"; uint16_t voc_baseline_state_[4]{0}; - uint32_t voc_baseline_time_; - uint16_t firmware_version_; + uint32_t voc_baseline_time_{0}; + uint16_t firmware_version_{0}; Sen5xType type_{Sen5xType::UNKNOWN}; - ERRORCODE error_code_; + ERRORCODE error_code_{ERRORCODE::UNKNOWN}; bool initialized_{false}; - bool store_baseline_; + bool store_baseline_{false}; sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index 44d0a54080..cb41e374f8 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -199,7 +199,7 @@ void SGP4xComponent::measure_raw_() { 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); // first parameter are the relative humidity ticks data[0] = rhticks; diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index a2da686ce1..e9e2f66d78 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -107,11 +107,11 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { std::string recipient_; std::string outgoing_message_; std::string ussd_; - bool send_pending_; - bool dial_pending_; - bool connect_pending_; - bool disconnect_pending_; - bool send_ussd_pending_; + bool send_pending_{false}; + bool dial_pending_{false}; + bool connect_pending_{false}; + bool disconnect_pending_{false}; + bool send_ussd_pending_{false}; uint8_t call_state_{6}; CallbackManager sms_received_callback_; diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 2b60497474..bd3632802b 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -90,7 +90,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { }; struct timezone tz = {0, 0}; int ret = settimeofday(&timev, &tz); - if (ret == EINVAL) { + if (ret != 0 && errno == EINVAL) { // Some ESP8266 frameworks abort when timezone parameter is not NULL // while ESP32 expects it not to be NULL ret = settimeofday(&timev, nullptr); diff --git a/esphome/components/tm1638/tm1638.cpp b/esphome/components/tm1638/tm1638.cpp index 8ef546ff32..c67ff1adbc 100644 --- a/esphome/components/tm1638/tm1638.cpp +++ b/esphome/components/tm1638/tm1638.cpp @@ -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 pos = start_pos; - bool last_was_dot = false; for (; *str != '\0'; str++) { uint8_t data = TM1638_UNKNOWN_CHAR; if (*str >= ' ' && *str <= '~') { - data = progmem_read_byte(&TM1638Translation::SEVEN_SEG[*str - 32]); // subract 32 to account for ASCII offset - } else if (data == TM1638_UNKNOWN_CHAR) { + // Subtract 32 to account for ASCII offset + data = progmem_read_byte(&TM1638Translation::SEVEN_SEG[*str - 32]); + } else { ESP_LOGW(TAG, "Encountered character '%c' with no TM1638 representation while translating string!", *str); } - if (*str == '.') // handle dots - { - if (pos != start_pos && - !last_was_dot) // if we are not at the first position, backup by one unless last char was a dot - { + if (*str == '.') { + // Merge dot onto previous character unless we're at the start or last was also a dot + if (pos != start_pos && !last_was_dot) { pos--; } - this->buffer_[pos] |= 0b10000000; // turn on the dot on the previous position - last_was_dot = true; // set a bit in case the next chracter is also a dot - } else // if not a dot, then just write the character to display - { + if (pos >= 8) { + ESP_LOGI(TAG, "TM1638 String is too long for the 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) { ESP_LOGI(TAG, "TM1638 String is too long for the display!"); break; } this->buffer_[pos] = data; - last_was_dot = false; // clear dot tracking bit + last_was_dot = false; } pos++; diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index 42c524a074..4ce673a91a 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -327,7 +327,9 @@ uint16_t TSL2591Component::get_illuminance(TSL2591SensorChannel channel, uint32_ return (combined_illuminance >> 16); } else if (channel == TSL2591_SENSOR_CHANNEL_VISIBLE) { // 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! ESP_LOGE(TAG, "get_illuminance() caller requested an unknown channel: %d", channel); diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 3274640eb3..818dae06de 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -11,7 +11,7 @@ static const char *const TAG = "entity_base"; // Entity 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); if (this->name_.empty()) { #ifdef USE_DEVICES @@ -44,17 +44,19 @@ void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, ui this->calc_object_id_(); } } - // Unpack entity string table indices. - // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) + // Unpack entity string table indices and flags from entity_fields. #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 #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 #ifdef USE_ENTITY_ICON - this->icon_idx_ = (entity_strings_packed >> 16) & 0xFF; + this->icon_idx_ = (entity_fields >> ENTITY_FIELD_ICON_SHIFT) & 0xFF; #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 diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index accd532b0d..cccbafd2c3 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -55,6 +55,15 @@ enum EntityCategory : uint8_t { 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. class EntityBase { public: @@ -88,21 +97,16 @@ class EntityBase { /// Useful for building compound strings without intermediate buffer 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; } - void set_internal(bool internal) { this->flags_.internal = internal; } // 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 // 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; } - 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(this->flags_.entity_category); } - void set_entity_category(EntityCategory entity_category) { - this->flags_.entity_category = static_cast(entity_category); - } // Get this entity's device class into a stack buffer. // On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused). @@ -164,14 +168,13 @@ class EntityBase { #endif #ifdef USE_DEVICES - // Get/set this entity's device id + // Get this entity's device id uint32_t get_device_id() const { if (this->device_ == nullptr) { return 0; // No device set, return 0 } 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) Device *get_device() const { return this->device_; } #endif @@ -228,8 +231,14 @@ class EntityBase { friend void ::setup(); friend void ::original_setup(); - /// Combined entity setup from codegen: set name, object_id hash, and entity string indices. - void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed); + /// Combined entity setup from codegen: set name, object_id hash, entity string indices, and flags. + /// 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. /// When preference hash algorithm changes, migration logic goes here. diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 4fa109fb0e..a3d6a3b2fe 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -34,11 +34,19 @@ _KEY_ICON_IDX = "_entity_icon_idx" _KEY_ENTITY_NAME = "_entity_name" _KEY_OBJECT_ID_HASH = "_entity_object_id_hash" -# Bit layout for entity_strings_packed in configure_entity_() — must match C++ in entity_base.h: -# [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits) +# Bit layout for entity_fields in configure_entity_(). +# Keep in sync with ENTITY_FIELD_*_SHIFT constants in esphome/core/entity_base.h _DC_SHIFT = 0 _UOM_SHIFT = 8 _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) _MAX_DEVICE_CLASSES = 0xFF # 255 @@ -220,8 +228,39 @@ def setup_unit_of_measurement(config: ConfigType) -> None: 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: - """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 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) uom_idx = config.get(_KEY_UOM_IDX, 0) icon_idx = config.get(_KEY_ICON_IDX, 0) - packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT) - add(var.configure_entity_(entity_name, object_id_hash, packed)) + internal = config.get(_KEY_INTERNAL, 0) + 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( @@ -332,7 +387,7 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> # 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)) + add(var.set_device_(device)) # Pre-compute entity name and object_id hash for configure_entity_() # 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 config[_KEY_ENTITY_NAME] = entity_name config[_KEY_OBJECT_ID_HASH] = 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)) + # Store flags for packing into configure_entity_() + config[_KEY_DISABLED_BY_DEFAULT] = int(config[CONF_DISABLED_BY_DEFAULT]) if CONF_INTERNAL in config: - add(var.set_internal(config[CONF_INTERNAL])) + config[_KEY_INTERNAL] = int(config[CONF_INTERNAL]) icon_idx = 0 if CONF_ICON in config: # Add USE_ENTITY_ICON define when icons are used cg.add_define("USE_ENTITY_ICON") icon_idx = register_icon(config[CONF_ICON]) 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 config[_KEY_ICON_IDX] = icon_idx diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index fbc2f37d9a..2667e90dda 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -45,8 +45,9 @@ def test_binary_sensor_config_value_internal_set(generate_main): ) # Then - assert "bs_1->set_internal(true);" in main_cpp - assert "bs_2->set_internal(false);" in main_cpp + # internal flag is now packed into configure_entity_() third argument (bit 24) + 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): diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index 9f94d61c8c..cd767dd65b 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -40,5 +40,7 @@ def test_button_config_value_internal_set(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert "wol_1->set_internal(true);" in main_cpp - assert "wol_2->set_internal(false);" in main_cpp + # internal flag is packed into configure_entity_() third argument (bit 24) + # 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 diff --git a/tests/component_tests/sensor/test_sensor.py b/tests/component_tests/sensor/test_sensor.py index 1fd9322c07..d9ab3a022c 100644 --- a/tests/component_tests/sensor/test_sensor.py +++ b/tests/component_tests/sensor/test_sensor.py @@ -1,5 +1,15 @@ """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): """ @@ -10,5 +20,6 @@ def test_sensor_device_class_set(generate_main): # When main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml") - # Then - assert "s_1->configure_entity_(" in main_cpp + # Then: device_class: voltage means packed value must be non-zero + packed = _extract_packed_value(main_cpp, "s_1") + assert packed != 0 diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 3ceaa9b8f8..bad7ad3a33 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -38,8 +38,9 @@ def test_text_config_value_internal_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_2->set_internal(false);" in main_cpp - assert "it_3->set_internal(true);" in main_cpp + # internal flag is now packed into configure_entity_() third argument (bit 24) + 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): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index cdbb9d2b66..21a273b947 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -1,5 +1,15 @@ """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): """ @@ -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") # Then - assert "ts_2->set_internal(true);" in main_cpp - assert "ts_3->set_internal(false);" in main_cpp + # internal flag is now packed into configure_entity_() third argument (bit 24) + 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): @@ -53,6 +64,9 @@ def test_text_sensor_device_class_set(generate_main): # When main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") - # Then - assert "ts_2->configure_entity_(" in main_cpp - assert "ts_3->configure_entity_(" in main_cpp + # Then: ts_2 has device_class: timestamp, ts_3 has device_class: date + # so their packed values must be non-zero + 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 diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 1169f5baa3..a68955fe08 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -9,6 +9,7 @@ import pytest from esphome.config_validation import Invalid from esphome.const import ( + CONF_DEVICE_CLASS, CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ENTITY_CATEGORY, @@ -16,16 +17,26 @@ from esphome.const import ( CONF_ID, CONF_INTERNAL, CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, ) from esphome.core import CORE, ID, entity_helpers from esphome.core.entity_helpers import ( + _DC_SHIFT, + _DISABLED_BY_DEFAULT_SHIFT, + _ENTITY_CATEGORY_SHIFT, + _ICON_SHIFT, + _INTERNAL_SHIFT, + _UOM_SHIFT, _register_string, _setup_entity_impl, entity_duplicate_validator, + finalize_entity_strings, get_base_entity_object_id, register_device_class, register_icon, + setup_device_class, setup_entity, + setup_unit_of_measurement, ) from esphome.cpp_generator import MockObj 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: """Test setup_entity with unique names.""" - setup_test_environment # noqa: F841 - fixture initializes CORE state - # Create mock entities var1 = MockObj("sensor1") var2 = MockObj("sensor2") @@ -344,8 +353,6 @@ async def test_setup_entity_different_platforms( ) -> None: """Test that same name on different platforms doesn't conflict.""" - setup_test_environment # noqa: F841 - fixture initializes CORE state - # Create mock entities sensor = MockObj("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] ) -> None: """Test that same name on different devices doesn't conflict.""" - setup_test_environment # noqa: F841 - fixture initializes CORE state # Create mock devices 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: """Test setup_entity with empty entity name.""" - setup_test_environment # noqa: F841 - fixture initializes CORE state - var = MockObj("sensor1") config = { @@ -455,8 +459,6 @@ async def test_setup_entity_special_characters( ) -> None: """Test setup_entity with names containing special characters.""" - setup_test_environment # noqa: F841 - fixture initializes CORE state - var = MockObj("sensor1") 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: """Test setup_entity sets icon correctly.""" - setup_test_environment # noqa: F841 - fixture initializes CORE state - var = MockObj("sensor1") config = { @@ -497,8 +497,6 @@ async def test_setup_entity_disabled_by_default( ) -> None: """Test setup_entity sets disabled_by_default correctly.""" - added_expressions = setup_test_environment - var = MockObj("sensor1") config = { @@ -508,10 +506,8 @@ async def test_setup_entity_disabled_by_default( await _setup_entity_impl(var, config, "sensor") - # Check disabled_by_default was set - assert any( - "sensor1.set_disabled_by_default(true)" in expr for expr in added_expressions - ) + # disabled_by_default is now packed into config for configure_entity_() + assert config.get("_entity_disabled_by_default") == 1 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 - # Check that set_device was called - assert any("sensor1.set_device" in expr for expr in added_expressions) + # Check that set_device_ was called (separate protected call, accessible via friend) + 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 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 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 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 (bug-for-bug compatibility). """ - setup_test_environment # noqa: F841 - fixture initializes CORE state # Set up CORE.config with name_add_mac_suffix enabled 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 at runtime from the device name. """ - setup_test_environment # noqa: F841 - fixture initializes CORE state # No MAC suffix (either not set or False) CORE.config = {} @@ -942,7 +935,7 @@ def test_register_device_class_max_length() -> None: async def test_setup_entity_with_entity_category( setup_test_environment: list[str], ) -> None: - """Test setup_entity sets entity_category correctly.""" + """Test entity_category is packed correctly through the full setup flow.""" added_expressions = setup_test_environment var = MockObj("sensor1") config = { @@ -951,9 +944,9 @@ async def test_setup_entity_with_entity_category( CONF_ENTITY_CATEGORY: "diagnostic", } await _setup_entity_impl(var, config, "sensor") - assert any( - 'set_entity_category("diagnostic")' in expr for expr in added_expressions - ) + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 2 @pytest.mark.asyncio @@ -1002,3 +995,200 @@ async def test_setup_entity_decorator_mode(setup_test_environment: list[str]) -> assert body_called object_id = extract_object_id_from_expressions(added_expressions) 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