1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-21 11:13:46 +01:00

Merge branch 'light_bitmask' into integration

This commit is contained in:
J. Nick Koston
2025-10-18 10:36:08 -10:00
11 changed files with 214 additions and 35 deletions

View File

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

View File

@@ -453,7 +453,6 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
bool is_single) {
auto *light = static_cast<light::LightState *>(entity);
LightStateResponse resp;
auto traits = light->get_traits();
auto values = light->remote_values;
auto color_mode = values.get_color_mode();
resp.state = values.is_on();
@@ -477,7 +476,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
auto *light = static_cast<light::LightState *>(entity);
ListEntitiesLightResponse msg;
auto traits = light->get_traits();
msg.supported_color_modes = &traits.get_supported_color_modes_for_api_();
msg.supported_color_modes = traits.get_supported_color_modes().get_mask();
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
msg.min_mireds = traits.get_min_mireds();

View File

@@ -70,4 +70,13 @@ extend google.protobuf.FieldOptions {
// init(size) before adding elements. This eliminates std::vector template overhead
// and is ideal when the exact size is known before populating the array.
optional bool fixed_vector = 50013 [default=false];
// enum_as_bitmask: Encode repeated enum fields as a uint32_t bitmask
// When set on a repeated enum field, the field will be stored as a single uint32_t
// where each bit represents whether that enum value is present. This is ideal for
// enums with ≤32 values and eliminates all vector template instantiation overhead.
// The enum values should be sequential starting from 0.
// Encoding: bit N set means enum value N is present in the set.
// Example: {ColorMode::RGB, ColorMode::WHITE} → bitmask with bits 5 and 6 set
optional bool enum_as_bitmask = 50014 [default=false];
}

View File

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

View File

@@ -790,7 +790,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_light_response"; }
#endif
const std::set<light::ColorMode> *supported_color_modes{};
uint32_t supported_color_modes{};
float min_mireds{0.0f};
float max_mireds{0.0f};
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, "key", this->key);
dump_field(out, "name", this->name_ref_);
for (const auto &it : *this->supported_color_modes) {
dump_field(out, "supported_color_modes", static_cast<enums::ColorMode>(it), 4);
}
out.append(" supported_color_modes: 0x");
out.append(uint32_to_string(this->supported_color_modes));
out.append("\n");
dump_field(out, "min_mireds", this->min_mireds);
dump_field(out, "max_mireds", this->max_mireds);
for (const auto &it : this->effects) {

View File

@@ -104,5 +104,139 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) {
return static_cast<ColorMode>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
}
/// Bitmask for storing a set of ColorMode values efficiently.
/// Replaces std::set<ColorMode> to eliminate red-black tree overhead (~586 bytes).
class ColorModeMask {
public:
constexpr ColorModeMask() = default;
/// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE}
constexpr ColorModeMask(std::initializer_list<ColorMode> modes) {
for (auto mode : modes) {
this->add(mode);
}
}
constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); }
constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; }
constexpr size_t size() const {
// Count set bits
uint16_t n = this->mask_;
size_t count = 0;
while (n) {
count += n & 1;
n >>= 1;
}
return count;
}
/// Iterator support for API encoding
class Iterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = ColorMode;
using difference_type = std::ptrdiff_t;
using pointer = const ColorMode *;
using reference = ColorMode;
constexpr Iterator(uint16_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); }
constexpr ColorMode operator*() const { return bit_to_mode(bit_); }
constexpr Iterator &operator++() {
++bit_;
advance_to_next_set_bit_();
return *this;
}
constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; }
constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
private:
constexpr void advance_to_next_set_bit_() {
while (bit_ < 16 && !(mask_ & (1 << bit_))) {
++bit_;
}
}
uint16_t mask_;
int bit_;
};
constexpr Iterator begin() const { return Iterator(mask_, 0); }
constexpr Iterator end() const { return Iterator(mask_, 16); }
/// Get the raw bitmask value for API encoding
constexpr uint16_t get_mask() const { return this->mask_; }
private:
// 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.
// 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).
uint16_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 esphome

View File

@@ -406,7 +406,7 @@ void LightCall::transform_parameters_() {
}
}
ColorMode LightCall::compute_color_mode_() {
auto supported_modes = this->parent_->get_traits().get_supported_color_modes();
const auto &supported_modes = this->parent_->get_traits().get_supported_color_modes();
int supported_count = supported_modes.size();
// Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown.
@@ -425,10 +425,10 @@ 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
// 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.
std::set<ColorMode> suitable_modes = this->get_suitable_color_modes_();
ColorModeMask suitable_modes = this->get_suitable_color_modes_();
// Don't change if the current mode is suitable.
if (suitable_modes.count(current_mode) > 0) {
if (suitable_modes.contains(current_mode)) {
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)));
return current_mode;
@@ -436,7 +436,7 @@ ColorMode LightCall::compute_color_mode_() {
// Use the preferred suitable mode.
for (auto mode : suitable_modes) {
if (supported_modes.count(mode) == 0)
if (!supported_modes.contains(mode))
continue;
ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(),
@@ -451,7 +451,7 @@ ColorMode LightCall::compute_color_mode_() {
LOG_STR_ARG(color_mode_to_human(color_mode)));
return color_mode;
}
std::set<ColorMode> LightCall::get_suitable_color_modes_() {
ColorModeMask LightCall::get_suitable_color_modes_() {
bool has_white = this->has_white() && this->white_ > 0.0f;
bool has_ct = this->has_color_temperature();
bool has_cwww =

View File

@@ -1,7 +1,6 @@
#pragma once
#include "light_color_values.h"
#include <set>
namespace esphome {
@@ -187,7 +186,7 @@ class LightCall {
//// Compute the color mode that should be used for this call.
ColorMode compute_color_mode_();
/// Get potential color modes for this light call.
std::set<ColorMode> get_suitable_color_modes_();
ColorModeMask get_suitable_color_modes_();
/// Some color modes also can be set using non-native parameters, transform those calls.
void transform_parameters_();

View File

@@ -2,7 +2,6 @@
#include "esphome/core/helpers.h"
#include "color_mode.h"
#include <set>
namespace esphome {
@@ -19,12 +18,15 @@ class LightTraits {
public:
LightTraits() = default;
const std::set<ColorMode> &get_supported_color_modes() const { return this->supported_color_modes_; }
void set_supported_color_modes(std::set<ColorMode> supported_color_modes) {
this->supported_color_modes_ = std::move(supported_color_modes);
const ColorModeMask &get_supported_color_modes() const { return this->supported_color_modes_; }
void set_supported_color_modes(ColorModeMask supported_color_modes) {
this->supported_color_modes_ = supported_color_modes;
}
void set_supported_color_modes(std::initializer_list<ColorMode> modes) {
this->supported_color_modes_ = ColorModeMask(modes);
}
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(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 {
for (auto mode : this->supported_color_modes_) {
if (mode & color_capability)
@@ -59,19 +61,9 @@ class LightTraits {
void set_max_mireds(float max_mireds) { this->max_mireds_ = max_mireds; }
protected:
#ifdef USE_API
// The API connection is a friend class to access internal methods
friend class api::APIConnection;
// This method returns a reference to the internal color modes set.
// It is used by the API to avoid copying data when encoding messages.
// Warning: Do not use this method outside of the API connection code.
// It returns a reference to internal data that can be invalidated.
const std::set<ColorMode> &get_supported_color_modes_for_api_() const { return this->supported_color_modes_; }
#endif
std::set<ColorMode> supported_color_modes_{};
float min_mireds_{0};
float max_mireds_{0};
ColorModeMask supported_color_modes_{};
};
} // namespace light

View File

@@ -1418,6 +1418,8 @@ class RepeatedTypeInfo(TypeInfo):
self._use_pointer = bool(self._container_type)
# Check if this should use FixedVector instead of std::vector
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
# but we can't call create_field_type_info as it would cause recursion
@@ -1434,6 +1436,9 @@ class RepeatedTypeInfo(TypeInfo):
@property
def cpp_type(self) -> str:
if self._use_bitmask:
# For bitmask fields, store as a single uint32_t
return "uint32_t"
if self._use_pointer and self._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>)
@@ -1466,6 +1471,12 @@ class RepeatedTypeInfo(TypeInfo):
# Pointer fields don't support decoding
if self._use_pointer:
return None
if self._use_bitmask:
# For bitmask fields, decode enum value and set corresponding bit
content = self._ti.decode_varint
if content is None:
return None
return f"case {self.number}: this->{self.field_name} |= (1U << static_cast<uint8_t>({content})); break;"
content = self._ti.decode_varint
if content is None:
return None
@@ -1519,6 +1530,18 @@ class RepeatedTypeInfo(TypeInfo):
@property
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
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:
# 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"
@@ -1538,6 +1561,13 @@ class RepeatedTypeInfo(TypeInfo):
@property
def dump_content(self) -> str:
if self._use_bitmask:
# For bitmask fields, dump the hex value of the bitmask
return (
f'out.append(" {self.field_name}: 0x");\n'
f"out.append(uint32_to_string(this->{self.field_name}));\n"
f'out.append("\\n");'
)
if self._use_pointer:
# For pointer fields, dereference and use the existing helper
return _generate_array_dump_content(
@@ -1554,6 +1584,18 @@ class RepeatedTypeInfo(TypeInfo):
# 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
if self._use_bitmask:
# For bitmask fields, iterate through set bits and calculate size
# Each set bit encodes one enum value (as varint)
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
if isinstance(self._ti, MessageType):
field_id_size = self._ti.calculate_field_id_size()