mirror of
https://github.com/esphome/esphome.git
synced 2026-09-28 15:30:21 +00:00
[light] Shrink LightState by 12 bytes per light (#19711)
This commit is contained in:
@@ -70,6 +70,7 @@ from .types import ( # noqa: F401
|
||||
AddressableLightState,
|
||||
ChannelColors,
|
||||
ColorMode,
|
||||
GammaTable,
|
||||
LightOutput,
|
||||
LightState,
|
||||
LightStateRTCState,
|
||||
@@ -85,6 +86,7 @@ CODEOWNERS = ["@esphome/core"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
DOMAIN = "light"
|
||||
CONF_GAMMA_TABLE_ID = "gamma_table_id"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -140,18 +142,29 @@ def generate_gamma_table(gamma_correct: float) -> list[HexInt]:
|
||||
return [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)]
|
||||
|
||||
|
||||
def _get_or_create_gamma_table(gamma_correct):
|
||||
def gamma_table_initializer(gamma_correct: float) -> str:
|
||||
"""C++ initializer for a light::GammaTable: the lookup table, then gamma * 100."""
|
||||
lut = ", ".join(f"0x{int(v):04X}" for v in generate_gamma_table(gamma_correct))
|
||||
# gamma_x100 is a uint16_t; platforms that redefine gamma_correct leave it unbounded, so saturate here
|
||||
return f"{{{{{lut}}}, {min(0xFFFF, round(gamma_correct * 100))}}}"
|
||||
|
||||
|
||||
def _get_or_create_gamma_table(gamma_correct: float, table_id: ID) -> cg.RawExpression:
|
||||
data = _get_data()
|
||||
if gamma_correct in data.gamma_tables:
|
||||
return data.gamma_tables[gamma_correct]
|
||||
|
||||
forward = generate_gamma_table(gamma_correct)
|
||||
|
||||
gamma_str = f"{gamma_correct}".replace(".", "_")
|
||||
fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16)
|
||||
fwd_arr = cg.progmem_array(fwd_id, forward)
|
||||
data.gamma_tables[gamma_correct] = fwd_arr
|
||||
return fwd_arr
|
||||
# table_id is generated and resolved against every declared ID, so it can't collide with a
|
||||
# YAML ID; lights sharing a gamma reuse the first light's table.
|
||||
cg.add(
|
||||
cg.RawStatement(
|
||||
f"static constexpr light::GammaTable {table_id} PROGMEM = "
|
||||
f"{gamma_table_initializer(gamma_correct)};"
|
||||
)
|
||||
)
|
||||
table = cg.RawExpression(f"&{table_id}")
|
||||
data.gamma_tables[gamma_correct] = table
|
||||
return table
|
||||
|
||||
|
||||
def find_effect_index(effects: list, effect_name: str) -> int | None:
|
||||
@@ -357,6 +370,7 @@ LIGHT_SCHEMA = (
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(LightState),
|
||||
cv.GenerateID(CONF_GAMMA_TABLE_ID): cv.declare_id(GammaTable),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt.MQTTJSONLightComponent
|
||||
),
|
||||
@@ -553,14 +567,14 @@ async def setup_light_core_(light_var, config, output_var):
|
||||
) is not None and flash_transition_length != cv.time_period(
|
||||
DEFAULT_FLASH_TRANSITION_LENGTH
|
||||
):
|
||||
cg.add_define("USE_LIGHT_FLASH_TRANSITION_LENGTH")
|
||||
cg.add(light_var.set_flash_transition_length(flash_transition_length))
|
||||
# Setting an interval opts this light in and compiles the feature in
|
||||
if (interval := config.get(CONF_TRANSITION_STATE_PUBLISH_INTERVAL)) is not None:
|
||||
cg.add(light_var.set_transition_state_publish_interval(interval))
|
||||
cg.add_define("USE_LIGHT_TRANSITION_PUBLISH_INTERVAL")
|
||||
if (gamma_correct := config.get(CONF_GAMMA_CORRECT)) is not None:
|
||||
cg.add(light_var.set_gamma_correct(gamma_correct))
|
||||
fwd_arr = _get_or_create_gamma_table(gamma_correct)
|
||||
fwd_arr = _get_or_create_gamma_table(gamma_correct, config[CONF_GAMMA_TABLE_ID])
|
||||
cg.add(light_var.set_gamma_table(fwd_arr))
|
||||
cg.add_define("USE_LIGHT_GAMMA_LUT")
|
||||
effects = await cg.build_registry_list(
|
||||
|
||||
@@ -545,12 +545,18 @@ async def addressable_flicker_effect_to_code(config, effect_id):
|
||||
return var
|
||||
|
||||
|
||||
# LightState stores the active effect index in a uint16_t
|
||||
MAX_EFFECTS = 65535
|
||||
|
||||
|
||||
def validate_effects(allowed_effects):
|
||||
@schema_extractor("effects")
|
||||
def validator(value):
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return (allowed_effects, EFFECTS_REGISTRY)
|
||||
|
||||
if isinstance(value, list) and len(value) > MAX_EFFECTS:
|
||||
raise cv.Invalid(f"A light supports at most {MAX_EFFECTS} effects")
|
||||
value = cv.validate_registry("effect", EFFECTS_REGISTRY)(value)
|
||||
errors = []
|
||||
names = set()
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
|
||||
namespace esphome::light {
|
||||
|
||||
/// A gamma curve as codegen emits it into PROGMEM: the lookup table and the gamma it was built from
|
||||
struct GammaTable {
|
||||
uint16_t lut[256];
|
||||
uint16_t gamma_x100;
|
||||
};
|
||||
|
||||
/// Binary search a monotonically increasing uint16[256] PROGMEM table.
|
||||
/// Returns the largest index where table[index] <= target.
|
||||
inline uint8_t gamma_table_reverse_search(const uint16_t *table, uint16_t target) {
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "light_output.h"
|
||||
#include "transformers.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace esphome::light {
|
||||
|
||||
static const char *const TAG = "light";
|
||||
@@ -94,10 +96,17 @@ void LightState::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Light '%s'", this->get_name().c_str());
|
||||
auto traits = this->get_traits();
|
||||
if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) {
|
||||
#ifdef USE_LIGHT_GAMMA_LUT
|
||||
// Read the stored gamma * 100 directly so dump_config does not pull in get_gamma_correct()
|
||||
const unsigned gamma_x100 =
|
||||
this->gamma_table_ != nullptr ? progmem_read_uint16(&this->gamma_table_->gamma_x100) : 0;
|
||||
#else
|
||||
const unsigned gamma_x100 = 0;
|
||||
#endif
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Default Transition Length: %.1fs\n"
|
||||
" Gamma Correct: %.2f",
|
||||
this->default_transition_length_ / 1e3f, this->gamma_correct_);
|
||||
" Gamma Correct: %u.%02u",
|
||||
this->default_transition_length_ / 1e3f, gamma_x100 / 100, gamma_x100 % 100);
|
||||
#ifdef USE_LIGHT_TRANSITION_PUBLISH_INTERVAL
|
||||
// The define is build wide; only lights that set the option have an interval
|
||||
if (this->transition_state_publish_interval_ != 0) {
|
||||
@@ -296,6 +305,14 @@ void LightState::current_values_as_ct(float *color_temperature, float *white_bri
|
||||
*white_brightness = this->gamma_correct_lut(*white_brightness);
|
||||
}
|
||||
|
||||
float LightState::get_gamma_correct() const {
|
||||
#ifdef USE_LIGHT_GAMMA_LUT
|
||||
if (this->gamma_table_ != nullptr)
|
||||
return progmem_read_uint16(&this->gamma_table_->gamma_x100) * 0.01f;
|
||||
#endif // USE_LIGHT_GAMMA_LUT
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
#ifdef USE_LIGHT_GAMMA_LUT
|
||||
float LightState::gamma_correct_lut(float value) const {
|
||||
if (value <= 0.0f)
|
||||
@@ -307,10 +324,10 @@ float LightState::gamma_correct_lut(float value) const {
|
||||
float scaled = value * 255.0f;
|
||||
auto idx = static_cast<uint8_t>(scaled);
|
||||
if (idx >= 255)
|
||||
return progmem_read_uint16(&this->gamma_table_[255]) / 65535.0f;
|
||||
return progmem_read_uint16(&this->gamma_table_->lut[255]) / 65535.0f;
|
||||
float frac = scaled - idx;
|
||||
float a = progmem_read_uint16(&this->gamma_table_[idx]);
|
||||
float b = progmem_read_uint16(&this->gamma_table_[idx + 1]);
|
||||
float a = progmem_read_uint16(&this->gamma_table_->lut[idx]);
|
||||
float b = progmem_read_uint16(&this->gamma_table_->lut[idx + 1]);
|
||||
return (a + frac * (b - a)) / 65535.0f;
|
||||
}
|
||||
float LightState::gamma_uncorrect_lut(float value) const {
|
||||
@@ -321,12 +338,12 @@ float LightState::gamma_uncorrect_lut(float value) const {
|
||||
if (this->gamma_table_ == nullptr)
|
||||
return value;
|
||||
uint16_t target = static_cast<uint16_t>(value * 65535.0f);
|
||||
uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target);
|
||||
uint8_t lo = gamma_table_reverse_search(this->gamma_table_->lut, target);
|
||||
if (lo >= 255)
|
||||
return 1.0f;
|
||||
// Interpolate between lo and lo+1
|
||||
uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]);
|
||||
uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]);
|
||||
uint16_t a = progmem_read_uint16(&this->gamma_table_->lut[lo]);
|
||||
uint16_t b = progmem_read_uint16(&this->gamma_table_->lut[lo + 1]);
|
||||
if (b == a)
|
||||
return lo / 255.0f;
|
||||
float frac = static_cast<float>(target - a) / static_cast<float>(b - a);
|
||||
@@ -335,11 +352,14 @@ float LightState::gamma_uncorrect_lut(float value) const {
|
||||
#endif // USE_LIGHT_GAMMA_LUT
|
||||
|
||||
void LightState::start_effect_(uint32_t effect_index) {
|
||||
// An external add_effects() can exceed the codegen cap; ignore an index the uint16_t can't hold
|
||||
if (effect_index > std::numeric_limits<uint16_t>::max())
|
||||
return;
|
||||
this->stop_effect_();
|
||||
if (effect_index == 0)
|
||||
return;
|
||||
|
||||
this->active_effect_index_ = effect_index;
|
||||
this->active_effect_index_ = static_cast<uint16_t>(effect_index);
|
||||
auto *effect = this->get_active_effect_();
|
||||
effect->start_internal();
|
||||
// Enable loop while effect is active
|
||||
@@ -435,7 +455,7 @@ void LightState::save_remote_values_() {
|
||||
saved.color_temp = this->remote_values.get_color_temperature();
|
||||
saved.cold_white = this->remote_values.get_cold_white();
|
||||
saved.warm_white = this->remote_values.get_warm_white();
|
||||
saved.effect = this->active_effect_index_;
|
||||
saved.effect = static_cast<uint32_t>(this->active_effect_index_); // the saved layout stays uint32_t
|
||||
this->rtc_.save(&saved);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "light_call.h"
|
||||
#include "esp_color_correction.h"
|
||||
#include "light_color_values.h"
|
||||
#include "light_effect.h"
|
||||
#include "light_traits.h"
|
||||
@@ -151,15 +152,28 @@ class LightState : public EntityBase, public Component {
|
||||
}
|
||||
uint32_t get_default_transition_length() const { return this->default_transition_length_; }
|
||||
|
||||
/// Set the flash transition length
|
||||
#ifdef USE_LIGHT_FLASH_TRANSITION_LENGTH
|
||||
/// Set the flash transition length; only compiled in when a light configures one
|
||||
void set_flash_transition_length(uint32_t flash_transition_length) {
|
||||
this->flash_transition_length_ = flash_transition_length;
|
||||
}
|
||||
uint32_t get_flash_transition_length() const { return this->flash_transition_length_; }
|
||||
#else
|
||||
// Remove before 2027.4.0
|
||||
ESPDEPRECATED("set_flash_transition_length() does nothing unless flash_transition_length is set in YAML. Removed in "
|
||||
"2027.4.0",
|
||||
"2026.10.0")
|
||||
void set_flash_transition_length(uint32_t flash_transition_length) {}
|
||||
uint32_t get_flash_transition_length() const { return 0; }
|
||||
#endif
|
||||
|
||||
/// Set the gamma correction factor
|
||||
void set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; }
|
||||
float get_gamma_correct() const { return this->gamma_correct_; }
|
||||
// Remove before 2027.4.0
|
||||
ESPDEPRECATED("set_gamma_correct() does nothing; gamma is fixed at build time by gamma_correct in YAML. Removed in "
|
||||
"2027.4.0",
|
||||
"2026.10.0")
|
||||
void set_gamma_correct(float gamma_correct) {}
|
||||
/// The gamma correction factor, read from the entry after the gamma lookup table; 0 without one
|
||||
float get_gamma_correct() const;
|
||||
|
||||
#ifdef USE_LIGHT_TRANSITION_PUBLISH_INTERVAL
|
||||
void set_transition_state_publish_interval(uint32_t transition_state_publish_interval) {
|
||||
@@ -169,11 +183,11 @@ class LightState : public EntityBase, public Component {
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIGHT_GAMMA_LUT
|
||||
/// Set pre-computed gamma forward lookup table (256-entry uint16 PROGMEM array)
|
||||
void set_gamma_table(const uint16_t *forward) { this->gamma_table_ = forward; }
|
||||
/// Set the pre-computed PROGMEM gamma curve
|
||||
void set_gamma_table(const GammaTable *table) { this->gamma_table_ = table; }
|
||||
|
||||
/// Get the forward gamma lookup table
|
||||
const uint16_t *get_gamma_table() const { return this->gamma_table_; }
|
||||
/// Get the forward gamma lookup table, 256 PROGMEM entries
|
||||
const uint16_t *get_gamma_table() const { return this->gamma_table_ != nullptr ? this->gamma_table_->lut : nullptr; }
|
||||
|
||||
/// Apply gamma correction using the pre-computed forward LUT
|
||||
float gamma_correct_lut(float value) const;
|
||||
@@ -362,31 +376,31 @@ class LightState : public EntityBase, public Component {
|
||||
/// Values live in flash as function body; no per-instance data storage beyond this pointer.
|
||||
void (*state_callback_)(LightStateRTCState &, bool restored){nullptr};
|
||||
|
||||
/// 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.
|
||||
uint32_t default_transition_length_{};
|
||||
#ifdef USE_LIGHT_FLASH_TRANSITION_LENGTH
|
||||
/// Transition length to use for flash transitions.
|
||||
uint32_t flash_transition_length_{}; // Keep in sync with DEFAULT_FLASH_TRANSITION_LENGTH in __init__.py
|
||||
#endif
|
||||
#ifdef USE_LIGHT_TRANSITION_PUBLISH_INTERVAL
|
||||
uint32_t transition_state_publish_interval_{0};
|
||||
uint32_t last_transition_state_publish_{0};
|
||||
#endif
|
||||
/// Gamma correction factor for the light.
|
||||
float gamma_correct_{};
|
||||
#ifdef USE_LIGHT_GAMMA_LUT
|
||||
const uint16_t *gamma_table_{nullptr};
|
||||
const GammaTable *gamma_table_{nullptr};
|
||||
#endif // USE_LIGHT_GAMMA_LUT
|
||||
|
||||
/// 1-based index of the active effect, 0 if none; codegen caps effects at MAX_EFFECTS in effects.py
|
||||
uint16_t active_effect_index_{};
|
||||
/// Whether the light value should be written in the next cycle.
|
||||
bool next_write_{true};
|
||||
bool next_write_{true}; // a plain bool: it is the most written flag, and still shares the index's word
|
||||
// for effects, true if a transformer (transition) is active.
|
||||
bool is_transformer_active_{false};
|
||||
bool is_transformer_active_ : 1 {false};
|
||||
/// Whether this light persists its state to preferences at all.
|
||||
bool save_enabled_{false};
|
||||
bool save_enabled_ : 1 {false};
|
||||
#ifdef USE_LIGHT_TRANSITION_PUBLISH_INTERVAL
|
||||
/// True while the active transformer publishes current_values on an interval from loop().
|
||||
bool transition_publish_enabled_{false};
|
||||
bool transition_publish_enabled_ : 1 {false};
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import esphome.codegen as cg
|
||||
# Base
|
||||
light_ns = cg.esphome_ns.namespace("light")
|
||||
LightState = light_ns.class_("LightState", cg.EntityBase, cg.Component)
|
||||
GammaTable = light_ns.struct("GammaTable")
|
||||
LightStateRef = LightState.operator("ref")
|
||||
AddressableLightState = light_ns.class_("AddressableLightState", LightState)
|
||||
LightOutput = light_ns.class_("LightOutput")
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
#define USE_JSON
|
||||
#define USE_RADIO_FREQUENCY
|
||||
#define USE_LIGHT
|
||||
#define USE_LIGHT_FLASH_TRANSITION_LENGTH
|
||||
#define USE_LIGHT_GAMMA_LUT
|
||||
#define USE_LIGHT_TRANSITION_PUBLISH_INTERVAL
|
||||
#define USE_LOCK
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.light import generate_gamma_table
|
||||
from esphome.components.light import gamma_table_initializer
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
@@ -12,15 +12,13 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code(config):
|
||||
await original_to_code(config)
|
||||
cg.add_define("USE_LIGHT_GAMMA_LUT")
|
||||
# Use the light component's own generate_gamma_table() so the
|
||||
# Use the light component's own gamma_table_initializer() so the
|
||||
# benchmark stays in sync with any formula changes.
|
||||
forward = generate_gamma_table(2.8)
|
||||
values = ", ".join(f"0x{int(v):04X}" for v in forward)
|
||||
# Use extern-visible (non-static) array so the benchmark .cpp
|
||||
# can reference it via extern declaration.
|
||||
# Extern-visible (non-static) so the benchmark .cpp can reference it.
|
||||
cg.add_global(
|
||||
cg.RawStatement(
|
||||
f"extern const uint16_t bench_gamma_2_8_fwd[256] PROGMEM = {{{values}}};"
|
||||
"extern const esphome::light::GammaTable bench_gamma_2_8 PROGMEM = "
|
||||
f"{gamma_table_initializer(2.8)};"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#include "esphome/components/light/light_state.h"
|
||||
|
||||
// Gamma 2.8 forward LUT generated by the light component's Python codegen
|
||||
// (see tests/benchmarks/components/light/__init__.py which calls generate_gamma_table())
|
||||
extern const uint16_t bench_gamma_2_8_fwd[256];
|
||||
// (see tests/benchmarks/components/light/__init__.py which calls gamma_table_initializer())
|
||||
extern const esphome::light::GammaTable bench_gamma_2_8;
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
@@ -37,8 +37,7 @@ static void setup_rgbww_light(BenchLightOutput &output, TestLightState &light) {
|
||||
output.traits_.set_max_mireds(500.0f);
|
||||
light.configure("test_light");
|
||||
light.set_default_transition_length(0);
|
||||
light.set_gamma_correct(2.8f);
|
||||
light.set_gamma_table(bench_gamma_2_8_fwd);
|
||||
light.set_gamma_table(&bench_gamma_2_8);
|
||||
}
|
||||
|
||||
// --- LightCall::perform() with instant RGB color change (Home Assistant API path) ---
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Limits and flash layout that let LightState stay small."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.light.effects import (
|
||||
MAX_EFFECTS,
|
||||
MONOCHROMATIC_EFFECTS,
|
||||
validate_effects,
|
||||
)
|
||||
|
||||
|
||||
def _effects(count: int) -> list[dict[str, dict[str, str]]]:
|
||||
return [{"pulse": {"name": f"Pulse {i}"}} for i in range(count)]
|
||||
|
||||
|
||||
def test_rejects_more_effects_than_the_index_holds() -> None:
|
||||
with pytest.raises(cv.Invalid, match=f"at most {MAX_EFFECTS} effects"):
|
||||
validate_effects(MONOCHROMATIC_EFFECTS)(_effects(MAX_EFFECTS + 1))
|
||||
|
||||
|
||||
def test_accepts_a_normal_effect_list() -> None:
|
||||
assert len(validate_effects(MONOCHROMATIC_EFFECTS)(_effects(3))) == 3
|
||||
|
||||
|
||||
def test_gamma_table_initializer_holds_the_lut_then_gamma_times_100() -> None:
|
||||
from esphome.components.light import gamma_table_initializer, generate_gamma_table
|
||||
|
||||
init = gamma_table_initializer(2.8)
|
||||
lut = ", ".join(f"0x{int(v):04X}" for v in generate_gamma_table(2.8))
|
||||
assert init == f"{{{{{lut}}}, 280}}"
|
||||
|
||||
|
||||
def test_gamma_table_initializer_saturates_gamma_that_overflows_uint16() -> None:
|
||||
from esphome.components.light import gamma_table_initializer
|
||||
|
||||
assert gamma_table_initializer(1000.0).endswith(", 65535}")
|
||||
@@ -8,6 +8,8 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
cg.add_define("USE_WEBSERVER")
|
||||
# api_connection.cpp reports the port whenever USE_WEBSERVER is set
|
||||
cg.add_define("USE_WEBSERVER_PORT", 80)
|
||||
# The gamma table and its stored gamma are behind this define
|
||||
cg.add_define("USE_LIGHT_GAMMA_LUT")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
manifest.dependencies = manifest.dependencies + ["json"]
|
||||
|
||||
@@ -254,6 +254,7 @@ light:
|
||||
state: INITIAL
|
||||
brightness: INITIAL
|
||||
- platform: monochromatic
|
||||
flash_transition_length: 100ms
|
||||
id: test_monochromatic_light
|
||||
name: Monochromatic Light
|
||||
output: test_ledc_1
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/light/light_effect.h"
|
||||
#include "esphome/components/light/light_output.h"
|
||||
#include "esphome/components/light/light_state.h"
|
||||
|
||||
namespace esphome::light::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
class BrightnessOutput : public LightOutput {
|
||||
public:
|
||||
LightTraits get_traits() override {
|
||||
LightTraits traits;
|
||||
traits.set_supported_color_modes({ColorMode::BRIGHTNESS});
|
||||
return traits;
|
||||
}
|
||||
void write_state(LightState *state) override {}
|
||||
};
|
||||
|
||||
class NoopEffect : public LightEffect {
|
||||
public:
|
||||
using LightEffect::LightEffect;
|
||||
void apply() override {}
|
||||
};
|
||||
|
||||
// start_effect_() is where the uint32_t index is narrowed to the stored uint16_t.
|
||||
class TestableLightState : public LightState {
|
||||
public:
|
||||
using LightState::LightState;
|
||||
using LightState::start_effect_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// add_effects() is public, so an external component can exceed the codegen cap on effect count;
|
||||
// an index the uint16_t can't hold must be ignored rather than wrap onto another effect.
|
||||
TEST(LightStateEffect, IndexAboveUint16IsIgnoredAndKeepsTheActiveEffect) {
|
||||
BrightnessOutput output;
|
||||
TestableLightState state(&output);
|
||||
NoopEffect effect("Noop");
|
||||
state.add_effects({&effect});
|
||||
|
||||
state.start_effect_(1);
|
||||
ASSERT_EQ(state.get_current_effect_index(), 1u);
|
||||
|
||||
state.start_effect_(0x10000u); // unchecked narrowing wraps this to 0, which stops the effect
|
||||
EXPECT_EQ(state.get_current_effect_index(), 1u);
|
||||
}
|
||||
|
||||
// get_gamma_correct() reads the gamma codegen stores after the lookup table, rounded to two decimals.
|
||||
TEST(LightStateGamma, ReadsTheGammaStoredWithTheTable) {
|
||||
static constexpr GammaTable TABLE{{}, 280};
|
||||
BrightnessOutput output;
|
||||
LightState state(&output);
|
||||
state.set_gamma_table(&TABLE);
|
||||
EXPECT_FLOAT_EQ(state.get_gamma_correct(), 2.8f);
|
||||
EXPECT_EQ(state.get_gamma_table(), TABLE.lut);
|
||||
}
|
||||
|
||||
TEST(LightStateGamma, IsZeroWithoutATable) {
|
||||
BrightnessOutput output;
|
||||
LightState state(&output);
|
||||
EXPECT_FLOAT_EQ(state.get_gamma_correct(), 0.0f);
|
||||
EXPECT_EQ(state.get_gamma_table(), nullptr);
|
||||
}
|
||||
|
||||
} // namespace esphome::light::testing
|
||||
Reference in New Issue
Block a user