mirror of
https://github.com/esphome/esphome.git
synced 2026-10-06 19:06:37 +00:00
Merge branch 'protect_entity_setters' into integration
This commit is contained in:
@@ -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]);
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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++;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user