1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-23 20:23:50 +01:00

Merge branch 'light_bitmask' into integration

This commit is contained in:
J. Nick Koston
2025-10-18 14:26:06 -10:00
13 changed files with 234 additions and 174 deletions

View File

@@ -506,7 +506,7 @@ message ListEntitiesLightResponse {
string name = 3; string name = 3;
reserved 4; // Deprecated: was string unique_id reserved 4; // Deprecated: was string unique_id
repeated ColorMode supported_color_modes = 12 [(enum_as_bitmask) = true]; repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"];
// next four supports_* are for legacy clients, newer clients should use color modes // next four supports_* are for legacy clients, newer clients should use color modes
// Deprecated in API version 1.6 // Deprecated in API version 1.6
bool legacy_supports_brightness = 5 [deprecated=true]; bool legacy_supports_brightness = 5 [deprecated=true];

View File

@@ -476,9 +476,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
auto *light = static_cast<light::LightState *>(entity); auto *light = static_cast<light::LightState *>(entity);
ListEntitiesLightResponse msg; ListEntitiesLightResponse msg;
auto traits = light->get_traits(); auto traits = light->get_traits();
// msg.supported_color_modes is uint32_t, but get_mask() returns uint16_t // Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
// The upper 16 bits are zero-extended during assignment (ColorMode only has 10 values) msg.supported_color_modes = &traits.get_supported_color_modes();
msg.supported_color_modes = traits.get_supported_color_modes().get_mask();
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
msg.min_mireds = traits.get_min_mireds(); msg.min_mireds = traits.get_min_mireds();

View File

@@ -71,12 +71,13 @@ extend google.protobuf.FieldOptions {
// and is ideal when the exact size is known before populating the array. // and is ideal when the exact size is known before populating the array.
optional bool fixed_vector = 50013 [default=false]; optional bool fixed_vector = 50013 [default=false];
// enum_as_bitmask: Encode repeated enum fields as a uint32_t bitmask // container_pointer_no_template: Use a non-template container type for repeated fields
// When set on a repeated enum field, the field will be stored as a single uint32_t // Similar to container_pointer, but for containers that don't take template parameters.
// where each bit represents whether that enum value is present. This is ideal for // The container type is used as-is without appending element type.
// enums with ≤32 values and eliminates all vector template instantiation overhead. // The container must have:
// The enum values should be sequential starting from 0. // - begin() and end() methods returning iterators
// Encoding: bit N set means enum value N is present in the set. // - empty() method
// Example: {ColorMode::RGB, ColorMode::WHITE} → bitmask with bits 5 and 6 set // Example: [(container_pointer_no_template) = "light::ColorModeMask"]
optional bool enum_as_bitmask = 50014 [default=false]; // generates: const light::ColorModeMask *supported_color_modes{};
optional string container_pointer_no_template = 50014;
} }

View File

@@ -471,10 +471,8 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id_ref_); buffer.encode_string(1, this->object_id_ref_);
buffer.encode_fixed32(2, this->key); buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name_ref_); buffer.encode_string(3, this->name_ref_);
for (uint8_t bit = 0; bit < 32; bit++) { for (const auto &it : *this->supported_color_modes) {
if (this->supported_color_modes & (1U << bit)) { buffer.encode_uint32(12, static_cast<uint32_t>(it), true);
buffer.encode_uint32(12, bit, true);
}
} }
buffer.encode_float(9, this->min_mireds); buffer.encode_float(9, this->min_mireds);
buffer.encode_float(10, this->max_mireds); buffer.encode_float(10, this->max_mireds);
@@ -494,11 +492,9 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->object_id_ref_.size()); size.add_length(1, this->object_id_ref_.size());
size.add_fixed32(1, this->key); size.add_fixed32(1, this->key);
size.add_length(1, this->name_ref_.size()); size.add_length(1, this->name_ref_.size());
if (this->supported_color_modes != 0) { if (!this->supported_color_modes->empty()) {
for (uint8_t bit = 0; bit < 32; bit++) { for (const auto &it : *this->supported_color_modes) {
if (this->supported_color_modes & (1U << bit)) { size.add_uint32_force(1, static_cast<uint32_t>(it));
size.add_uint32_force(1, static_cast<uint32_t>(bit));
}
} }
} }
size.add_float(1, this->min_mireds); size.add_float(1, this->min_mireds);

View File

@@ -790,7 +790,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_light_response"; } const char *message_name() const override { return "list_entities_light_response"; }
#endif #endif
uint32_t supported_color_modes{}; const light::ColorModeMask *supported_color_modes{};
float min_mireds{0.0f}; float min_mireds{0.0f};
float max_mireds{0.0f}; float max_mireds{0.0f};
std::vector<std::string> effects{}; std::vector<std::string> effects{};

View File

@@ -913,9 +913,9 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
dump_field(out, "object_id", this->object_id_ref_); dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key); dump_field(out, "key", this->key);
dump_field(out, "name", this->name_ref_); dump_field(out, "name", this->name_ref_);
char buffer[64]; for (const auto &it : *this->supported_color_modes) {
snprintf(buffer, sizeof(buffer), " supported_color_modes: 0x%08" PRIX32 "\n", this->supported_color_modes); dump_field(out, "supported_color_modes", static_cast<enums::ColorMode>(it), 4);
out.append(buffer); }
dump_field(out, "min_mireds", this->min_mireds); dump_field(out, "min_mireds", this->min_mireds);
dump_field(out, "max_mireds", this->max_mireds); dump_field(out, "max_mireds", this->max_mireds);
for (const auto &it : this->effects) { for (const auto &it : this->effects) {

View File

@@ -104,6 +104,76 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) {
return static_cast<ColorMode>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs)); return static_cast<ColorMode>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
} }
// Type alias for raw color mode bitmask values
using color_mode_bitmask_t = uint16_t;
// Constants for ColorMode count and bit range
static constexpr int COLOR_MODE_COUNT = 10; // UNKNOWN through RGB_COLD_WARM_WHITE
static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8; // Number of bits in bitmask type
// Compile-time array of all ColorMode values in declaration order
// Bit positions (0-9) map directly to enum declaration order
static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = {
ColorMode::UNKNOWN, // bit 0
ColorMode::ON_OFF, // bit 1
ColorMode::BRIGHTNESS, // bit 2
ColorMode::WHITE, // bit 3
ColorMode::COLOR_TEMPERATURE, // bit 4
ColorMode::COLD_WARM_WHITE, // bit 5
ColorMode::RGB, // bit 6
ColorMode::RGB_WHITE, // bit 7
ColorMode::RGB_COLOR_TEMPERATURE, // bit 8
ColorMode::RGB_COLD_WARM_WHITE, // bit 9
};
/// Map ColorMode enum values to bit positions (0-9)
/// Bit positions follow the enum declaration order
static constexpr int mode_to_bit(ColorMode mode) {
// Linear search through COLOR_MODES array
// Compiler optimizes this to efficient code since array is constexpr
for (int i = 0; i < COLOR_MODE_COUNT; ++i) {
if (COLOR_MODES[i] == mode)
return i;
}
return 0;
}
/// Map bit positions (0-9) to ColorMode enum values
/// Bit positions follow the enum declaration order
static constexpr ColorMode bit_to_mode(int bit) {
// Direct lookup in COLOR_MODES array
return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN;
}
/// Helper to compute capability bitmask at compile time
static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) {
color_mode_bitmask_t mask = 0;
uint8_t cap_bit = static_cast<uint8_t>(capability);
// Check each ColorMode to see if it has this capability
for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) {
uint8_t mode_val = static_cast<uint8_t>(bit_to_mode(bit));
if ((mode_val & cap_bit) != 0) {
mask |= (1 << bit);
}
}
return mask;
}
// Number of ColorCapability enum values
static constexpr int COLOR_CAPABILITY_COUNT = 6;
/// Compile-time lookup table mapping ColorCapability to bitmask
/// This array is computed at compile time using constexpr
static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = {
compute_capability_bitmask(ColorCapability::ON_OFF), // 1 << 0
compute_capability_bitmask(ColorCapability::BRIGHTNESS), // 1 << 1
compute_capability_bitmask(ColorCapability::WHITE), // 1 << 2
compute_capability_bitmask(ColorCapability::COLOR_TEMPERATURE), // 1 << 3
compute_capability_bitmask(ColorCapability::COLD_WARM_WHITE), // 1 << 4
compute_capability_bitmask(ColorCapability::RGB), // 1 << 5
};
/// Bitmask for storing a set of ColorMode values efficiently. /// Bitmask for storing a set of ColorMode values efficiently.
/// Replaces std::set<ColorMode> to eliminate red-black tree overhead (~586 bytes). /// Replaces std::set<ColorMode> to eliminate red-black tree overhead (~586 bytes).
class ColorModeMask { class ColorModeMask {
@@ -119,6 +189,13 @@ class ColorModeMask {
constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); } constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); }
/// Add multiple modes at once using initializer list
constexpr void add(std::initializer_list<ColorMode> modes) {
for (auto mode : modes) {
this->add(mode);
}
}
constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; } constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; }
constexpr size_t size() const { constexpr size_t size() const {
@@ -132,6 +209,8 @@ class ColorModeMask {
return count; return count;
} }
constexpr bool empty() const { return this->mask_ == 0; }
/// Iterator support for API encoding /// Iterator support for API encoding
class Iterator { class Iterator {
public: public:
@@ -141,7 +220,7 @@ class ColorModeMask {
using pointer = const ColorMode *; using pointer = const ColorMode *;
using reference = ColorMode; using reference = ColorMode;
constexpr Iterator(uint16_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); } constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); }
constexpr ColorMode operator*() const { return bit_to_mode(bit_); } constexpr ColorMode operator*() const { return bit_to_mode(bit_); }
@@ -156,86 +235,61 @@ class ColorModeMask {
constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
private: private:
constexpr void advance_to_next_set_bit_() { constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); }
while (bit_ < 16 && !(mask_ & (1 << bit_))) {
++bit_;
}
}
uint16_t mask_; color_mode_bitmask_t mask_;
int bit_; int bit_;
}; };
constexpr Iterator begin() const { return Iterator(mask_, 0); } constexpr Iterator begin() const { return Iterator(mask_, 0); }
constexpr Iterator end() const { return Iterator(mask_, 16); } constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); }
/// Get the raw bitmask value for API encoding /// Get the raw bitmask value for API encoding
constexpr uint16_t get_mask() const { return this->mask_; } constexpr color_mode_bitmask_t get_mask() const { return this->mask_; }
/// Find the next set bit in a bitmask starting from a given position
/// Returns the bit position, or MAX_BIT_INDEX if no more bits are set
static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) {
int bit = start_bit;
while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) {
++bit;
}
return bit;
}
/// Find the first set bit in a bitmask and return the corresponding ColorMode
/// Used for optimizing compute_color_mode_() intersection logic
static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) {
return bit_to_mode(find_next_set_bit(mask, 0));
}
/// Check if a ColorMode is present in a raw bitmask value
/// Useful for checking intersection results without creating a temporary ColorModeMask
static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) {
return (mask & (1 << mode_to_bit(mode))) != 0;
}
/// Check if any mode in the bitmask has a specific capability
/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB)
bool has_capability(ColorCapability capability) const {
// Lookup the pre-computed bitmask for this capability and check intersection with our mask
// ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5
// We need to convert the power-of-2 value to an index
uint8_t cap_val = static_cast<uint8_t>(capability);
int index = 0;
while (cap_val > 1) {
cap_val >>= 1;
++index;
}
return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0;
}
private: private:
// Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan). // Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan).
// Currently only 10 ColorMode values exist, so 16 bits is sufficient. // Currently only 10 ColorMode values exist, so 16 bits is sufficient.
// Can be changed to uint32_t if more than 16 color modes are needed in the future. // Can be changed to uint32_t if more than 16 color modes are needed in the future.
// Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes). // Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes).
uint16_t mask_{0}; color_mode_bitmask_t mask_{0};
/// Map ColorMode enum values to bit positions (0-9)
static constexpr int mode_to_bit(ColorMode mode) {
// Using switch instead of lookup table to avoid RAM usage on ESP8266
// The compiler optimizes this efficiently
switch (mode) {
case ColorMode::UNKNOWN:
return 0;
case ColorMode::ON_OFF:
return 1;
case ColorMode::BRIGHTNESS:
return 2;
case ColorMode::WHITE:
return 3;
case ColorMode::COLOR_TEMPERATURE:
return 4;
case ColorMode::COLD_WARM_WHITE:
return 5;
case ColorMode::RGB:
return 6;
case ColorMode::RGB_WHITE:
return 7;
case ColorMode::RGB_COLOR_TEMPERATURE:
return 8;
case ColorMode::RGB_COLD_WARM_WHITE:
return 9;
default:
return 0;
}
}
static constexpr ColorMode bit_to_mode(int bit) {
// Using switch instead of lookup table to avoid RAM usage on ESP8266
switch (bit) {
case 0:
return ColorMode::UNKNOWN;
case 1:
return ColorMode::ON_OFF;
case 2:
return ColorMode::BRIGHTNESS;
case 3:
return ColorMode::WHITE;
case 4:
return ColorMode::COLOR_TEMPERATURE;
case 5:
return ColorMode::COLD_WARM_WHITE;
case 6:
return ColorMode::RGB;
case 7:
return ColorMode::RGB_WHITE;
case 8:
return ColorMode::RGB_COLOR_TEMPERATURE;
case 9:
return ColorMode::RGB_COLD_WARM_WHITE;
default:
return ColorMode::UNKNOWN;
}
}
}; };
} // namespace light } // namespace light

View File

@@ -425,20 +425,19 @@ ColorMode LightCall::compute_color_mode_() {
// If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to // If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to
// pre-colormode clients and automations, but also for the MQTT API, where HA doesn't let us know which color mode // pre-colormode clients and automations, but also for the MQTT API, where HA doesn't let us know which color mode
// was used for some reason. // was used for some reason.
ColorModeMask suitable_modes = this->get_suitable_color_modes_(); // Compute intersection of suitable and supported modes using bitwise AND
color_mode_bitmask_t intersection = this->get_suitable_color_modes_mask_() & supported_modes.get_mask();
// Don't change if the current mode is suitable. // Don't change if the current mode is in the intersection (suitable AND supported)
if (suitable_modes.contains(current_mode)) { if (ColorModeMask::mask_contains(intersection, current_mode)) {
ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(), ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(),
LOG_STR_ARG(color_mode_to_human(current_mode))); LOG_STR_ARG(color_mode_to_human(current_mode)));
return current_mode; return current_mode;
} }
// Use the preferred suitable mode. // Use the preferred suitable mode.
for (auto mode : suitable_modes) { if (intersection != 0) {
if (!supported_modes.contains(mode)) ColorMode mode = ColorModeMask::first_mode_from_mask(intersection);
continue;
ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(),
LOG_STR_ARG(color_mode_to_human(mode))); LOG_STR_ARG(color_mode_to_human(mode)));
return mode; return mode;
@@ -451,7 +450,7 @@ ColorMode LightCall::compute_color_mode_() {
LOG_STR_ARG(color_mode_to_human(color_mode))); LOG_STR_ARG(color_mode_to_human(color_mode)));
return color_mode; return color_mode;
} }
ColorModeMask LightCall::get_suitable_color_modes_() { color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
bool has_white = this->has_white() && this->white_ > 0.0f; bool has_white = this->has_white() && this->white_ > 0.0f;
bool has_ct = this->has_color_temperature(); bool has_ct = this->has_color_temperature();
bool has_cwww = bool has_cwww =
@@ -459,36 +458,44 @@ ColorModeMask LightCall::get_suitable_color_modes_() {
bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) || bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) ||
(this->has_red() || this->has_green() || this->has_blue()); (this->has_red() || this->has_green() || this->has_blue());
// Build key from flags: [rgb][cwww][ct][white] // Build key from flags: [rgb][cwww][ct][white]
#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) #define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3)
uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb); uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb);
switch (key) { switch (key) {
case KEY(true, false, false, false): // white only case KEY(true, false, false, false): // white only
return {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, return ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::RGB_COLD_WARM_WHITE}; ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
case KEY(false, true, false, false): // ct only case KEY(false, true, false, false): // ct only
return {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, return ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
ColorMode::RGB_COLD_WARM_WHITE}; ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
case KEY(true, true, false, false): // white + ct case KEY(true, true, false, false): // white + ct
return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; return ColorModeMask(
{ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
case KEY(false, false, true, false): // cwww only case KEY(false, false, true, false): // cwww only
return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}; return ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
case KEY(false, false, false, false): // none case KEY(false, false, false, false): // none
return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE,
ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}; ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE})
.get_mask();
case KEY(true, false, false, true): // rgb + white case KEY(true, false, false, true): // rgb + white
return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
case KEY(false, true, false, true): // rgb + ct case KEY(false, true, false, true): // rgb + ct
case KEY(true, true, false, true): // rgb + white + ct case KEY(true, true, false, true): // rgb + white + ct
return {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; return ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
case KEY(false, false, true, true): // rgb + cwww case KEY(false, false, true, true): // rgb + cwww
return {ColorMode::RGB_COLD_WARM_WHITE}; return ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
case KEY(false, false, false, true): // rgb only case KEY(false, false, false, true): // rgb only
return {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; return ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
default: default:
return {}; // conflicting flags return 0; // conflicting flags
} }
#undef KEY #undef KEY

View File

@@ -185,8 +185,8 @@ class LightCall {
//// Compute the color mode that should be used for this call. //// Compute the color mode that should be used for this call.
ColorMode compute_color_mode_(); ColorMode compute_color_mode_();
/// Get potential color modes for this light call. /// Get potential color modes bitmask for this light call.
ColorModeMask get_suitable_color_modes_(); color_mode_bitmask_t get_suitable_color_modes_mask_();
/// Some color modes also can be set using non-native parameters, transform those calls. /// Some color modes also can be set using non-native parameters, transform those calls.
void transform_parameters_(); void transform_parameters_();

View File

@@ -28,11 +28,7 @@ class LightTraits {
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); } bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); }
bool supports_color_capability(ColorCapability color_capability) const { bool supports_color_capability(ColorCapability color_capability) const {
for (auto mode : this->supported_color_modes_) { return this->supported_color_modes_.has_capability(color_capability);
if (mode & color_capability)
return true;
}
return false;
} }
ESPDEPRECATED("get_supports_brightness() is deprecated, use color modes instead.", "v1.21") ESPDEPRECATED("get_supports_brightness() is deprecated, use color modes instead.", "v1.21")

View File

@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0 esptool==5.1.0
click==8.1.7 click==8.1.7
esphome-dashboard==20251013.0 esphome-dashboard==20251013.0
aioesphomeapi==42.1.0 aioesphomeapi==42.2.0
zeroconf==0.148.0 zeroconf==0.148.0
puremagic==1.30 puremagic==1.30
ruamel.yaml==0.18.15 # dashboard_import ruamel.yaml==0.18.15 # dashboard_import

View File

@@ -1415,11 +1415,15 @@ class RepeatedTypeInfo(TypeInfo):
super().__init__(field) super().__init__(field)
# Check if this is a pointer field by looking for container_pointer option # Check if this is a pointer field by looking for container_pointer option
self._container_type = get_field_opt(field, pb.container_pointer, "") self._container_type = get_field_opt(field, pb.container_pointer, "")
self._use_pointer = bool(self._container_type) # Check for non-template container pointer
self._container_no_template = get_field_opt(
field, pb.container_pointer_no_template, ""
)
self._use_pointer = bool(self._container_type) or bool(
self._container_no_template
)
# Check if this should use FixedVector instead of std::vector # Check if this should use FixedVector instead of std::vector
self._use_fixed_vector = get_field_opt(field, pb.fixed_vector, False) self._use_fixed_vector = get_field_opt(field, pb.fixed_vector, False)
# Check if this should be encoded as a bitmask
self._use_bitmask = get_field_opt(field, pb.enum_as_bitmask, False)
# For repeated fields, we need to get the base type info # For repeated fields, we need to get the base type info
# but we can't call create_field_type_info as it would cause recursion # but we can't call create_field_type_info as it would cause recursion
@@ -1436,15 +1440,18 @@ class RepeatedTypeInfo(TypeInfo):
@property @property
def cpp_type(self) -> str: def cpp_type(self) -> str:
if self._use_bitmask: if self._container_no_template:
# For bitmask fields, store as a single uint32_t # Non-template container: use type as-is without appending template parameters
return "uint32_t" return f"const {self._container_no_template}*"
if self._use_pointer and self._container_type: if self._use_pointer and self._container_type:
# For pointer fields, use the specified container type # For pointer fields, use the specified container type
# If the container type already includes the element type (e.g., std::set<climate::ClimateMode>) # Two cases:
# use it as-is, otherwise append the element type # 1. "std::set<climate::ClimateMode>" - Full type with template params, use as-is
# 2. "std::set" - No <>, append the element type
if "<" in self._container_type and ">" in self._container_type: if "<" in self._container_type and ">" in self._container_type:
# Has template parameters specified, use as-is
return f"const {self._container_type}*" return f"const {self._container_type}*"
# No <> at all, append element type
return f"const {self._container_type}<{self._ti.cpp_type}>*" return f"const {self._container_type}<{self._ti.cpp_type}>*"
if self._use_fixed_vector: if self._use_fixed_vector:
return f"FixedVector<{self._ti.cpp_type}>" return f"FixedVector<{self._ti.cpp_type}>"
@@ -1471,11 +1478,6 @@ class RepeatedTypeInfo(TypeInfo):
# Pointer fields don't support decoding # Pointer fields don't support decoding
if self._use_pointer: if self._use_pointer:
return None return None
if self._use_bitmask:
# Bitmask fields don't support decoding (only used for device->client messages)
raise RuntimeError(
f"enum_as_bitmask fields do not support decoding: {self.field_name}"
)
content = self._ti.decode_varint content = self._ti.decode_varint
if content is None: if content is None:
return None return None
@@ -1529,21 +1531,6 @@ class RepeatedTypeInfo(TypeInfo):
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
if self._use_bitmask:
# For bitmask fields, iterate through set bits and encode each enum value
# The bitmask is stored as uint32_t where bit N represents enum value N
# Note: We iterate through all 32 bits to support the full range of enum_as_bitmask
# (enums with up to 32 values). Specific uses may have fewer values, but the
# generated code is general-purpose.
assert isinstance(self._ti, EnumType), (
"enum_as_bitmask only works with enum fields"
)
o = "for (uint8_t bit = 0; bit < 32; bit++) {\n"
o += f" if (this->{self.field_name} & (1U << bit)) {{\n"
o += f" buffer.{self._ti.encode_func}({self.number}, bit, true);\n"
o += " }\n"
o += "}"
return o
if self._use_pointer: if self._use_pointer:
# For pointer fields, just dereference (pointer should never be null in our use case) # For pointer fields, just dereference (pointer should never be null in our use case)
o = f"for (const auto &it : *this->{self.field_name}) {{\n" o = f"for (const auto &it : *this->{self.field_name}) {{\n"
@@ -1563,13 +1550,6 @@ class RepeatedTypeInfo(TypeInfo):
@property @property
def dump_content(self) -> str: def dump_content(self) -> str:
if self._use_bitmask:
# For bitmask fields, dump the hex value of the bitmask
return (
f"char buffer[64];\n"
f'snprintf(buffer, sizeof(buffer), " {self.field_name}: 0x%08" PRIX32 "\\n", this->{self.field_name});\n'
f"out.append(buffer);"
)
if self._use_pointer: if self._use_pointer:
# For pointer fields, dereference and use the existing helper # For pointer fields, dereference and use the existing helper
return _generate_array_dump_content( return _generate_array_dump_content(
@@ -1586,21 +1566,6 @@ class RepeatedTypeInfo(TypeInfo):
# For repeated fields, we always need to pass force=True to the underlying type's calculation # For repeated fields, we always need to pass force=True to the underlying type's calculation
# This is because the encode method always sets force=true for repeated fields # This is because the encode method always sets force=true for repeated fields
if self._use_bitmask:
# For bitmask fields, iterate through set bits and calculate size
# Each set bit encodes one enum value (as varint)
# Note: We iterate through all 32 bits to support the full range of enum_as_bitmask
# (enums with up to 32 values). Specific uses may have fewer values, but the
# generated code is general-purpose.
o = f"if ({name} != 0) {{\n"
o += " for (uint8_t bit = 0; bit < 32; bit++) {\n"
o += f" if ({name} & (1U << bit)) {{\n"
o += f" {self._ti.get_size_calculation('bit', True)}\n"
o += " }\n"
o += " }\n"
o += "}"
return o
# Handle message types separately as they use a dedicated helper # Handle message types separately as they use a dedicated helper
if isinstance(self._ti, MessageType): if isinstance(self._ti, MessageType):
field_id_size = self._ti.calculate_field_id_size() field_id_size = self._ti.calculate_field_id_size()

View File

@@ -8,6 +8,7 @@ import asyncio
from typing import Any from typing import Any
from aioesphomeapi import LightState from aioesphomeapi import LightState
from aioesphomeapi.model import ColorMode
import pytest import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -35,10 +36,51 @@ async def test_light_calls(
# Get the light entities # Get the light entities
entities = await client.list_entities_services() entities = await client.list_entities_services()
lights = [e for e in entities[0] if e.object_id.startswith("test_")] lights = [e for e in entities[0] if e.object_id.startswith("test_")]
assert len(lights) >= 2 # Should have RGBCW and RGB lights assert len(lights) >= 3 # Should have RGBCW, RGB, and Binary lights
rgbcw_light = next(light for light in lights if "RGBCW" in light.name) rgbcw_light = next(light for light in lights if "RGBCW" in light.name)
rgb_light = next(light for light in lights if "RGB Light" in light.name) rgb_light = next(light for light in lights if "RGB Light" in light.name)
binary_light = next(light for light in lights if "Binary" in light.name)
# Test color mode encoding: Verify supported_color_modes contains actual ColorMode enum values
# not bit positions. This is critical - the iterator must convert bit positions to actual
# ColorMode enum values for API encoding.
# RGBCW light (rgbww platform) should support RGB_COLD_WARM_WHITE mode
assert ColorMode.RGB_COLD_WARM_WHITE in rgbcw_light.supported_color_modes, (
f"RGBCW light missing RGB_COLD_WARM_WHITE mode. Got: {rgbcw_light.supported_color_modes}"
)
# Verify it's the actual enum value, not bit position
assert ColorMode.RGB_COLD_WARM_WHITE.value in [
mode.value for mode in rgbcw_light.supported_color_modes
], (
f"RGBCW light has wrong color mode values. Expected {ColorMode.RGB_COLD_WARM_WHITE.value} "
f"(RGB_COLD_WARM_WHITE), got: {[mode.value for mode in rgbcw_light.supported_color_modes]}"
)
# RGB light should support RGB mode
assert ColorMode.RGB in rgb_light.supported_color_modes, (
f"RGB light missing RGB color mode. Got: {rgb_light.supported_color_modes}"
)
# Verify it's the actual enum value, not bit position
assert ColorMode.RGB.value in [
mode.value for mode in rgb_light.supported_color_modes
], (
f"RGB light has wrong color mode values. Expected {ColorMode.RGB.value} (RGB), got: "
f"{[mode.value for mode in rgb_light.supported_color_modes]}"
)
# Binary light (on/off only) should support ON_OFF mode
assert ColorMode.ON_OFF in binary_light.supported_color_modes, (
f"Binary light missing ON_OFF color mode. Got: {binary_light.supported_color_modes}"
)
# Verify it's the actual enum value, not bit position
assert ColorMode.ON_OFF.value in [
mode.value for mode in binary_light.supported_color_modes
], (
f"Binary light has wrong color mode values. Expected {ColorMode.ON_OFF.value} (ON_OFF), got: "
f"{[mode.value for mode in binary_light.supported_color_modes]}"
)
async def wait_for_state_change(key: int, timeout: float = 1.0) -> Any: async def wait_for_state_change(key: int, timeout: float = 1.0) -> Any:
"""Wait for a state change for the given entity key.""" """Wait for a state change for the given entity key."""