Reduce LightCall memory usage by 50 bytes per call (#9333)

This commit is contained in:
J. Nick Koston
2025-07-07 15:20:40 -05:00
committed by GitHub
parent 3ef392d433
commit 31f36df4ba
9 changed files with 522 additions and 263 deletions
+3 -3
View File
@@ -97,12 +97,12 @@ class AddressableLight : public LightOutput, public Component {
}
virtual ESPColorView get_view_internal(int32_t index) const = 0;
bool effect_active_{false};
ESPColorCorrection correction_{};
LightState *state_parent_{nullptr};
#ifdef USE_POWER_SUPPLY
power_supply::PowerSupplyRequester power_;
#endif
LightState *state_parent_{nullptr};
bool effect_active_{false};
};
class AddressableLightTransformer : public LightTransitionTransformer {
@@ -114,9 +114,9 @@ class AddressableLightTransformer : public LightTransitionTransformer {
protected:
AddressableLight &light_;
Color target_color_{};
float last_transition_progress_{0.0f};
float accumulated_alpha_{0.0f};
Color target_color_{};
};
} // namespace light
@@ -69,8 +69,8 @@ class ESPColorCorrection {
protected:
uint8_t gamma_table_[256];
uint8_t gamma_reverse_table_[256];
uint8_t local_brightness_{255};
Color max_brightness_;
uint8_t local_brightness_{255};
};
} // namespace light
File diff suppressed because it is too large Load Diff
+72 -20
View File
@@ -1,6 +1,5 @@
#pragma once
#include "esphome/core/optional.h"
#include "light_color_values.h"
#include <set>
@@ -10,6 +9,11 @@ namespace light {
class LightState;
/** This class represents a requested change in a light state.
*
* Light state changes are tracked using a bitfield flags_ to minimize memory usage.
* Each possible light property has a flag indicating whether it has been set.
* This design keeps LightCall at ~56 bytes to minimize heap fragmentation on
* ESP8266 and other memory-constrained devices.
*/
class LightCall {
public:
@@ -131,6 +135,19 @@ class LightCall {
/// Set whether this light call should trigger a save state to recover them at startup..
LightCall &set_save(bool save);
// Getter methods to check if values are set
bool has_state() const { return (flags_ & FLAG_HAS_STATE) != 0; }
bool has_brightness() const { return (flags_ & FLAG_HAS_BRIGHTNESS) != 0; }
bool has_color_brightness() const { return (flags_ & FLAG_HAS_COLOR_BRIGHTNESS) != 0; }
bool has_red() const { return (flags_ & FLAG_HAS_RED) != 0; }
bool has_green() const { return (flags_ & FLAG_HAS_GREEN) != 0; }
bool has_blue() const { return (flags_ & FLAG_HAS_BLUE) != 0; }
bool has_white() const { return (flags_ & FLAG_HAS_WHITE) != 0; }
bool has_color_temperature() const { return (flags_ & FLAG_HAS_COLOR_TEMPERATURE) != 0; }
bool has_cold_white() const { return (flags_ & FLAG_HAS_COLD_WHITE) != 0; }
bool has_warm_white() const { return (flags_ & FLAG_HAS_WARM_WHITE) != 0; }
bool has_color_mode() const { return (flags_ & FLAG_HAS_COLOR_MODE) != 0; }
/** Set the RGB color of the light by RGB values.
*
* Please note that this only changes the color of the light, not the brightness.
@@ -170,27 +187,62 @@ class LightCall {
/// Some color modes also can be set using non-native parameters, transform those calls.
void transform_parameters_();
bool has_transition_() { return this->transition_length_.has_value(); }
bool has_flash_() { return this->flash_length_.has_value(); }
bool has_effect_() { return this->effect_.has_value(); }
// Bitfield flags - each flag indicates whether a corresponding value has been set.
enum FieldFlags : uint16_t {
FLAG_HAS_STATE = 1 << 0,
FLAG_HAS_TRANSITION = 1 << 1,
FLAG_HAS_FLASH = 1 << 2,
FLAG_HAS_EFFECT = 1 << 3,
FLAG_HAS_BRIGHTNESS = 1 << 4,
FLAG_HAS_COLOR_BRIGHTNESS = 1 << 5,
FLAG_HAS_RED = 1 << 6,
FLAG_HAS_GREEN = 1 << 7,
FLAG_HAS_BLUE = 1 << 8,
FLAG_HAS_WHITE = 1 << 9,
FLAG_HAS_COLOR_TEMPERATURE = 1 << 10,
FLAG_HAS_COLD_WHITE = 1 << 11,
FLAG_HAS_WARM_WHITE = 1 << 12,
FLAG_HAS_COLOR_MODE = 1 << 13,
FLAG_PUBLISH = 1 << 14,
FLAG_SAVE = 1 << 15,
};
bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; }
bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; }
bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
// Helper to set flag
void set_flag_(FieldFlags flag, bool value) {
if (value) {
this->flags_ |= flag;
} else {
this->flags_ &= ~flag;
}
}
LightState *parent_;
optional<bool> state_;
optional<uint32_t> transition_length_;
optional<uint32_t> flash_length_;
optional<ColorMode> color_mode_;
optional<float> brightness_;
optional<float> color_brightness_;
optional<float> red_;
optional<float> green_;
optional<float> blue_;
optional<float> white_;
optional<float> color_temperature_;
optional<float> cold_white_;
optional<float> warm_white_;
optional<uint32_t> effect_;
bool publish_{true};
bool save_{true};
// Light state values - use flags_ to check if a value has been set.
// Group 4-byte aligned members first
uint32_t transition_length_;
uint32_t flash_length_;
uint32_t effect_;
float brightness_;
float color_brightness_;
float red_;
float green_;
float blue_;
float white_;
float color_temperature_;
float cold_white_;
float warm_white_;
// Smaller members at the end for better packing
uint16_t flags_{FLAG_PUBLISH | FLAG_SAVE}; // Tracks which values are set
ColorMode color_mode_;
bool state_;
};
} // namespace light
@@ -46,8 +46,7 @@ class LightColorValues {
public:
/// Construct the LightColorValues with all attributes enabled, but state set to off.
LightColorValues()
: color_mode_(ColorMode::UNKNOWN),
state_(0.0f),
: state_(0.0f),
brightness_(1.0f),
color_brightness_(1.0f),
red_(1.0f),
@@ -56,7 +55,8 @@ class LightColorValues {
white_(1.0f),
color_temperature_{0.0f},
cold_white_{1.0f},
warm_white_{1.0f} {}
warm_white_{1.0f},
color_mode_(ColorMode::UNKNOWN) {}
LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green,
float blue, float white, float color_temperature, float cold_white, float warm_white) {
@@ -292,7 +292,6 @@ class LightColorValues {
void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
protected:
ColorMode color_mode_;
float state_; ///< ON / OFF, float for transition
float brightness_;
float color_brightness_;
@@ -303,6 +302,7 @@ class LightColorValues {
float color_temperature_; ///< Color Temperature in Mired
float cold_white_;
float warm_white_;
ColorMode color_mode_;
};
} // namespace light
+11 -10
View File
@@ -31,9 +31,7 @@ enum LightRestoreMode : uint8_t {
struct LightStateRTCState {
LightStateRTCState(ColorMode color_mode, bool state, float brightness, float color_brightness, float red, float green,
float blue, float white, float color_temp, float cold_white, float warm_white)
: color_mode(color_mode),
state(state),
brightness(brightness),
: brightness(brightness),
color_brightness(color_brightness),
red(red),
green(green),
@@ -41,10 +39,12 @@ struct LightStateRTCState {
white(white),
color_temp(color_temp),
cold_white(cold_white),
warm_white(warm_white) {}
warm_white(warm_white),
effect(0),
color_mode(color_mode),
state(state) {}
LightStateRTCState() = default;
ColorMode color_mode{ColorMode::UNKNOWN};
bool state{false};
// Group 4-byte aligned members first
float brightness{1.0f};
float color_brightness{1.0f};
float red{1.0f};
@@ -55,6 +55,9 @@ struct LightStateRTCState {
float cold_white{1.0f};
float warm_white{1.0f};
uint32_t effect{0};
// Group smaller members at the end
ColorMode color_mode{ColorMode::UNKNOWN};
bool state{false};
};
/** This class represents the communication layer between the front-end MQTT layer and the
@@ -216,6 +219,8 @@ class LightState : public EntityBase, public Component {
std::unique_ptr<LightTransformer> transformer_{nullptr};
/// List of effects for this light.
std::vector<LightEffect *> effects_;
/// Object used to store the persisted values of the light.
ESPPreferenceObject rtc_;
/// Value for storing the index of the currently active effect. 0 if no effect is active
uint32_t active_effect_index_{};
/// Default transition length for all transitions in ms.
@@ -224,15 +229,11 @@ class LightState : public EntityBase, public Component {
uint32_t flash_transition_length_{};
/// Gamma correction factor for the light.
float gamma_correct_{};
/// Whether the light value should be written in the next cycle.
bool next_write_{true};
// for effects, true if a transformer (transition) is active.
bool is_transformer_active_ = false;
/// Object used to store the persisted values of the light.
ESPPreferenceObject rtc_;
/** Callback to call when new values for the frontend are available.
*
* "Remote values" are light color values that are reported to the frontend and have a lower
+2 -2
View File
@@ -59,9 +59,9 @@ class LightTransitionTransformer : public LightTransformer {
// transition from 0 to 1 on x = [0, 1]
static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
bool changing_color_mode_{false};
LightColorValues end_values_{};
LightColorValues intermediate_values_{};
bool changing_color_mode_{false};
};
class LightFlashTransformer : public LightTransformer {
@@ -117,8 +117,8 @@ class LightFlashTransformer : public LightTransformer {
protected:
LightState &state_;
uint32_t transition_length_;
std::unique_ptr<LightTransformer> transformer_{nullptr};
uint32_t transition_length_;
bool begun_lightstate_restore_;
};
@@ -0,0 +1,80 @@
esphome:
name: light-calls-test
host:
api: # Port will be automatically injected
logger:
level: DEBUG
# Test outputs for RGBCW light
output:
- platform: template
id: test_red
type: float
write_action:
- logger.log:
format: "Red output: %.2f"
args: [state]
- platform: template
id: test_green
type: float
write_action:
- logger.log:
format: "Green output: %.2f"
args: [state]
- platform: template
id: test_blue
type: float
write_action:
- logger.log:
format: "Blue output: %.2f"
args: [state]
- platform: template
id: test_cold_white
type: float
write_action:
- logger.log:
format: "Cold white output: %.2f"
args: [state]
- platform: template
id: test_warm_white
type: float
write_action:
- logger.log:
format: "Warm white output: %.2f"
args: [state]
light:
- platform: rgbww
name: "Test RGBCW Light"
id: test_light
red: test_red
green: test_green
blue: test_blue
cold_white: test_cold_white
warm_white: test_warm_white
cold_white_color_temperature: 6536 K
warm_white_color_temperature: 2000 K
constant_brightness: true
effects:
- random:
name: "Random Effect"
transition_length: 100ms
update_interval: 200ms
- strobe:
name: "Strobe Effect"
- pulse:
name: "Pulse Effect"
transition_length: 100ms
# Additional lights to test memory with multiple instances
- platform: rgb
name: "Test RGB Light"
id: test_rgb_light
red: test_red
green: test_green
blue: test_blue
- platform: binary
name: "Test Binary Light"
id: test_binary_light
output: test_red
+189
View File
@@ -0,0 +1,189 @@
"""Integration test for all light call combinations.
Tests that LightCall handles all possible light operations correctly
including RGB, color temperature, effects, transitions, and flash.
"""
import asyncio
from typing import Any
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_light_calls(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test all possible LightCall operations and combinations."""
async with run_compiled(yaml_config), api_client_connected() as client:
# Track state changes with futures
state_futures: dict[int, asyncio.Future[Any]] = {}
states: dict[int, Any] = {}
def on_state(state: Any) -> None:
states[state.key] = state
if state.key in state_futures and not state_futures[state.key].done():
state_futures[state.key].set_result(state)
client.subscribe_states(on_state)
# Get the light entities
entities = await client.list_entities_services()
lights = [e for e in entities[0] if e.object_id.startswith("test_")]
assert len(lights) >= 2 # Should have RGBCW and RGB lights
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)
async def wait_for_state_change(key: int, timeout: float = 1.0) -> Any:
"""Wait for a state change for the given entity key."""
loop = asyncio.get_event_loop()
state_futures[key] = loop.create_future()
try:
return await asyncio.wait_for(state_futures[key], timeout)
finally:
state_futures.pop(key, None)
# Test all individual parameters first
# Test 1: state only
client.light_command(key=rgbcw_light.key, state=True)
state = await wait_for_state_change(rgbcw_light.key)
assert state.state is True
# Test 2: brightness only
client.light_command(key=rgbcw_light.key, brightness=0.5)
state = await wait_for_state_change(rgbcw_light.key)
assert state.brightness == pytest.approx(0.5)
# Test 3: color_brightness only
client.light_command(key=rgbcw_light.key, color_brightness=0.8)
state = await wait_for_state_change(rgbcw_light.key)
assert state.color_brightness == pytest.approx(0.8)
# Test 4-7: RGB values must be set together via rgb parameter
client.light_command(key=rgbcw_light.key, rgb=(0.7, 0.3, 0.9))
state = await wait_for_state_change(rgbcw_light.key)
assert state.red == pytest.approx(0.7, abs=0.1)
assert state.green == pytest.approx(0.3, abs=0.1)
assert state.blue == pytest.approx(0.9, abs=0.1)
# Test 7: white value
client.light_command(key=rgbcw_light.key, white=0.6)
state = await wait_for_state_change(rgbcw_light.key)
# White might need more tolerance or might not be directly settable
if hasattr(state, "white"):
assert state.white == pytest.approx(0.6, abs=0.1)
# Test 8: color_temperature only
client.light_command(key=rgbcw_light.key, color_temperature=300)
state = await wait_for_state_change(rgbcw_light.key)
assert state.color_temperature == pytest.approx(300)
# Test 9: cold_white only
client.light_command(key=rgbcw_light.key, cold_white=0.8)
state = await wait_for_state_change(rgbcw_light.key)
assert state.cold_white == pytest.approx(0.8)
# Test 10: warm_white only
client.light_command(key=rgbcw_light.key, warm_white=0.2)
state = await wait_for_state_change(rgbcw_light.key)
assert state.warm_white == pytest.approx(0.2)
# Test 11: transition_length with state change
client.light_command(key=rgbcw_light.key, state=False, transition_length=0.1)
state = await wait_for_state_change(rgbcw_light.key)
assert state.state is False
# Test 12: flash_length
client.light_command(key=rgbcw_light.key, state=True, flash_length=0.2)
state = await wait_for_state_change(rgbcw_light.key)
# Flash starts
assert state.state is True
# Wait for flash to end
state = await wait_for_state_change(rgbcw_light.key)
# Test 13: effect only
# First ensure light is on
client.light_command(key=rgbcw_light.key, state=True)
state = await wait_for_state_change(rgbcw_light.key)
# Now set effect
client.light_command(key=rgbcw_light.key, effect="Random Effect")
state = await wait_for_state_change(rgbcw_light.key)
assert state.effect == "Random Effect"
# Test 14: stop effect
client.light_command(key=rgbcw_light.key, effect="None")
state = await wait_for_state_change(rgbcw_light.key)
assert state.effect == "None"
# Test 15: color_mode parameter
client.light_command(
key=rgbcw_light.key, state=True, color_mode=5
) # COLD_WARM_WHITE
state = await wait_for_state_change(rgbcw_light.key)
assert state.state is True
# Now test common combinations
# Test 16: RGB combination (set_rgb) - RGB values get normalized
client.light_command(key=rgbcw_light.key, rgb=(1.0, 0.0, 0.5))
state = await wait_for_state_change(rgbcw_light.key)
# RGB values get normalized - in this case red is already 1.0
assert state.red == pytest.approx(1.0, abs=0.1)
assert state.green == pytest.approx(0.0, abs=0.1)
assert state.blue == pytest.approx(0.5, abs=0.1)
# Test 17: Multiple RGB changes to test transitions
client.light_command(key=rgbcw_light.key, rgb=(0.2, 0.8, 0.4))
state = await wait_for_state_change(rgbcw_light.key)
# RGB values get normalized so green (highest) becomes 1.0
# Expected: (0.2/0.8, 0.8/0.8, 0.4/0.8) = (0.25, 1.0, 0.5)
assert state.red == pytest.approx(0.25, abs=0.01)
assert state.green == pytest.approx(1.0, abs=0.01)
assert state.blue == pytest.approx(0.5, abs=0.01)
# Test 18: State + brightness + transition
client.light_command(
key=rgbcw_light.key, state=True, brightness=0.7, transition_length=0.1
)
state = await wait_for_state_change(rgbcw_light.key)
assert state.state is True
assert state.brightness == pytest.approx(0.7)
# Test 19: RGB + brightness + color_brightness
client.light_command(
key=rgb_light.key,
state=True,
brightness=0.8,
color_brightness=0.9,
rgb=(0.2, 0.4, 0.6),
)
state = await wait_for_state_change(rgb_light.key)
assert state.state is True
assert state.brightness == pytest.approx(0.8)
# Test 20: Color temp + cold/warm white
client.light_command(
key=rgbcw_light.key, color_temperature=250, cold_white=0.7, warm_white=0.3
)
state = await wait_for_state_change(rgbcw_light.key)
assert state.color_temperature == pytest.approx(250)
# Test 21: Turn RGB light off
client.light_command(key=rgb_light.key, state=False)
state = await wait_for_state_change(rgb_light.key)
assert state.state is False
# Final cleanup - turn all lights off
for light in lights:
client.light_command(
key=light.key,
state=False,
)
state = await wait_for_state_change(light.key)
assert state.state is False