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:
@@ -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];
|
||||||
|
@@ -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();
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
@@ -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{};
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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_();
|
||||||
|
|
||||||
|
@@ -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")
|
||||||
|
@@ -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
|
||||||
|
@@ -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()
|
||||||
|
@@ -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."""
|
||||||
|
Reference in New Issue
Block a user