[output] Gate FloatOutput power scaling fields behind USE_OUTPUT_FLOAT_POWER_SCALING

The min_power / max_power / zero_means_zero scaling support on FloatOutput
costs 12 bytes per instance (max_power_, min_power_, zero_means_zero_ +
alignment padding) on every PWM channel, DAC channel, LEDC output, and
dimmer-chip channel — even on configs that never touch the feature.

Repo-wide usage is ~17 YAML lines, mostly in test fixtures and a couple
of LED-driver chip tests; the runtime set_min_power / set_max_power
actions added in #8934 have no usage outside the action's own test.

Add USE_OUTPUT_FLOAT_POWER_SCALING and gate the fields and scaling math
in FloatOutput::set_level() behind it, mirroring the USE_POWER_SUPPLY
pattern already used in BinaryOutput. Python codegen flips the define on
whenever:
- a min_power / max_power / zero_means_zero key is set on any output, or
- a non-default zero_means_zero value is provided, or
- an output.set_min_power / output.set_max_power action is registered

The action class templates (SetMinPowerAction, SetMaxPowerAction) are
also gated on the same define so their non-dependent member access on
FloatOutput::set_min_power doesn't fail to parse when the methods aren't
compiled in. zero_means_zero_ now has a default initializer (was UB
before — it was always written from setup, but only because the schema
default forced it).

For configs without scaling: 12 B .bss saved per FloatOutput instance,
plus a small flash saving from the elided multiply/subtract in
set_level(). For configs with scaling: behavior is unchanged.

Verified on tests/components/esp8266_pwm (no scaling): pstorage 0x28 → 0x1c
per output (40 B → 28 B). Verified on tests/components/output (uses
set_min_power/set_max_power actions): builds correctly with the define on.
This commit is contained in:
J. Nick Koston
2026-04-25 12:39:32 -05:00
parent b5ccd55f4e
commit 15cb2c0580
5 changed files with 34 additions and 2 deletions
+8 -1
View File
@@ -54,10 +54,15 @@ async def setup_output_platform_(obj, config):
power_supply_ = await cg.get_variable(config[CONF_POWER_SUPPLY])
cg.add(obj.set_power_supply(power_supply_))
if CONF_MAX_POWER in config:
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
cg.add(obj.set_max_power(config[CONF_MAX_POWER]))
if CONF_MIN_POWER in config:
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
cg.add(obj.set_min_power(config[CONF_MIN_POWER]))
if CONF_ZERO_MEANS_ZERO in config:
# Only emit (and pull in the scaling field) when explicitly enabled — the
# C++ default initializer covers the False case, saving 4 B per instance.
if config.get(CONF_ZERO_MEANS_ZERO):
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
cg.add(obj.set_zero_means_zero(config[CONF_ZERO_MEANS_ZERO]))
@@ -121,6 +126,7 @@ async def output_set_level_to_code(config, action_id, template_arg, args):
synchronous=True,
)
async def output_set_min_power_to_code(config, action_id, template_arg, args):
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_MIN_POWER], args, cg.float_)
@@ -140,6 +146,7 @@ async def output_set_min_power_to_code(config, action_id, template_arg, args):
synchronous=True,
)
async def output_set_max_power_to_code(config, action_id, template_arg, args):
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_MAX_POWER], args, cg.float_)
+3
View File
@@ -2,6 +2,7 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/defines.h"
#include "esphome/components/output/binary_output.h"
#include "esphome/components/output/float_output.h"
@@ -40,6 +41,7 @@ template<typename... Ts> class SetLevelAction : public Action<Ts...> {
FloatOutput *output_;
};
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
template<typename... Ts> class SetMinPowerAction : public Action<Ts...> {
public:
SetMinPowerAction(FloatOutput *output) : output_(output) {}
@@ -63,6 +65,7 @@ template<typename... Ts> class SetMaxPowerAction : public Action<Ts...> {
protected:
FloatOutput *output_;
};
#endif // USE_OUTPUT_FLOAT_POWER_SCALING
} // namespace output
} // namespace esphome
@@ -7,6 +7,7 @@ namespace output {
static const char *const TAG = "output.float";
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
void FloatOutput::set_max_power(float max_power) {
this->max_power_ = clamp(max_power, this->min_power_, 1.0f); // Clamp to MIN>=MAX>=1.0
}
@@ -14,6 +15,7 @@ void FloatOutput::set_max_power(float max_power) {
void FloatOutput::set_min_power(float min_power) {
this->min_power_ = clamp(min_power, 0.0f, this->max_power_); // Clamp to 0.0>=MIN>=MAX
}
#endif
void FloatOutput::set_level(float state) {
state = clamp(state, 0.0f, 1.0f);
@@ -26,8 +28,10 @@ void FloatOutput::set_level(float state) {
}
#endif
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
if (state != 0.0f || !this->zero_means_zero_) // regardless of min_power_, 0.0 means off
state = (state * (this->max_power_ - this->min_power_)) + this->min_power_;
#endif
if (this->is_inverted())
state = 1.0f - state;
+18 -1
View File
@@ -1,11 +1,13 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "binary_output.h"
namespace esphome {
namespace output {
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
#define LOG_FLOAT_OUTPUT(this) \
LOG_BINARY_OUTPUT(this) \
if (this->max_power_ != 1.0f) { \
@@ -14,6 +16,9 @@ namespace output {
if (this->min_power_ != 0.0f) { \
ESP_LOGCONFIG(TAG, " Min Power: %.1f%%", this->min_power_ * 100.0f); \
}
#else
#define LOG_FLOAT_OUTPUT(this) LOG_BINARY_OUTPUT(this)
#endif
/** Base class for all output components that can output a variable level, like PWM.
*
@@ -30,6 +35,7 @@ namespace output {
*/
class FloatOutput : public BinaryOutput {
public:
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
/** Set the maximum power output of this component.
*
* All values are multiplied by max_power - min_power and offset to min_power to get the adjusted value.
@@ -51,6 +57,7 @@ class FloatOutput : public BinaryOutput {
* @param zero_means_zero True if a 0 state should mean 0 and not min_power.
*/
void set_zero_means_zero(bool zero_means_zero) { this->zero_means_zero_ = zero_means_zero; }
#endif
/** Set the level of this float output, this is called from the front-end.
*
@@ -69,20 +76,30 @@ class FloatOutput : public BinaryOutput {
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
/// Get the maximum power output.
float get_max_power() const { return this->max_power_; }
/// Get the minimum power output.
float get_min_power() const { return this->min_power_; }
#else
/// Get the maximum power output.
float get_max_power() const { return 1.0f; }
/// Get the minimum power output.
float get_min_power() const { return 0.0f; }
#endif
protected:
/// Implement BinarySensor's write_enabled; this should never be called.
void write_state(bool state) override;
virtual void write_state(float state) = 0;
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
float max_power_{1.0f};
float min_power_{0.0f};
bool zero_means_zero_;
bool zero_means_zero_{false};
#endif
};
} // namespace output
+1
View File
@@ -132,6 +132,7 @@
#define USE_NEXTION_WAVEFORM
#define USE_NUMBER
#define USE_OUTPUT
#define USE_OUTPUT_FLOAT_POWER_SCALING
#define USE_POWER_SUPPLY
#define USE_PREFERENCES_SYNC_EVERY_LOOP
#define USE_QR_CODE