Compare commits

...
Author SHA1 Message Date
J. Nick Koston ff36d3a6c8 Fetch the bh1745 hub once 2026-10-20 17:48:01 -10:00
J. Nick Koston ba3bc70bbb Merge branch 'apply-entities-output' into apply-entities-output-migrate 2026-10-20 17:36:24 -10:00
J. Nick Koston 6c1a7642f3 Merge remote-tracking branch 'origin/dev' into apply-entities-output 2026-10-20 17:36:24 -10:00
J. Nick Koston 8b0d1d31db [pca9685] Use Apply outputs for the channels 2026-10-20 17:35:42 -10:00
J. Nick Koston 653686e11e Pass the hub channel as the Apply output index
Every channel of a hub now shares one generated function. Adds
write_channel to tlc5947, tlc5971 and write_pwm_level to
waveshare_io_ch32v003 so apply strings hold no C++ math, drops the
now unused float_output.h and helpers.h includes, and fetches each
hub once.
2026-10-20 17:35:42 -10:00
J. Nick Koston d17dcaaee4 Merge branch 'apply-entities-output' into apply-entities-output-migrate 2026-10-20 17:29:36 -10:00
J. Nick Koston 1ce0db00b3 [output] Share one Apply output instantiation per hub with a channel index 2026-10-20 17:29:33 -10:00
J. Nick Koston 6409611e1c Drop the component schema from the Apply outputs 2026-10-20 17:23:25 -10:00
J. Nick Koston 86dcbe4645 [components] Use Apply outputs for hub channel outputs 2026-10-20 17:22:22 -10:00
J. Nick Koston a50a30414e [output] Add ApplyFloatOutput and ApplyBinaryOutput 2026-10-20 17:09:32 -10:00
62 changed files with 392 additions and 721 deletions
+5 -9
View File
@@ -1,28 +1,24 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
from .. import CONF_BH1745_ID, BH1745Component, bh1745_ns
from .. import CONF_BH1745_ID, BH1745Component
DEPENDENCIES = ["bh1745"]
BH1745InterruptPinOutput = bh1745_ns.class_(
"BH1745InterruptPinOutput", output.BinaryOutput
)
CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(BH1745InterruptPinOutput),
cv.GenerateID(): cv.declare_id(output.ApplyBinaryOutput),
cv.GenerateID(CONF_BH1745_ID): cv.use_id(BH1745Component),
}
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
hub = await cg.get_variable(config[CONF_BH1745_ID])
await cg.register_parented(var, hub)
await output.sub_binary_outputs(config, parent=hub).create(
config, None, apply="set_interrupt_state(state)"
)
cg.add(hub.enable_interrupt_output())
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/output/binary_output.h"
#include "esphome/core/helpers.h"
#include "../bh1745.h"
namespace esphome::bh1745 {
/// Drives the BH1745 INT pin, which switches the LEDs on breakout boards such as the Pimoroni one.
class BH1745InterruptPinOutput : public output::BinaryOutput, public Parented<BH1745Component> {
protected:
void write_state(bool state) override { this->parent_->set_interrupt_state(state); }
};
} // namespace esphome::bh1745
+2 -1
View File
@@ -80,7 +80,8 @@ void BP1658CJ::loop() {
this->update_ = false;
}
void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) {
void BP1658CJ::write_channel(uint8_t channel, float state) {
auto value = static_cast<uint16_t>(state * 0x3FF);
if (this->pwm_amounts_[channel] != value) {
this->update_ = true;
this->update_channel_ = channel;
+1 -18
View File
@@ -2,15 +2,12 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/output/float_output.h"
#include <vector>
namespace esphome::bp1658cj {
class BP1658CJ final : public Component {
public:
class Channel;
void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; }
void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; }
void set_max_power_color_channels(uint8_t max_power_color_channels) {
@@ -29,23 +26,9 @@ class BP1658CJ final : public Component {
/// Send new values if they were updated.
void loop() override;
class Channel final : public output::FloatOutput {
public:
void set_parent(BP1658CJ *parent) { parent_ = parent; }
void set_channel(uint8_t channel) { channel_ = channel; }
protected:
void write_state(float state) override {
auto amount = static_cast<uint16_t>(state * 0x3FF);
this->parent_->set_channel_value_(this->channel_, amount);
}
BP1658CJ *parent_;
uint8_t channel_;
};
void write_channel(uint8_t channel, float state);
protected:
void set_channel_value_(uint8_t channel, uint16_t value);
void write_bit_(bool value);
void write_byte_(uint8_t data);
void write_buffer_(uint8_t *buffer, uint8_t size);
+5 -9
View File
@@ -1,4 +1,3 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
@@ -8,21 +7,18 @@ from . import BP1658CJ
DEPENDENCIES = ["bp1658cj"]
Channel = BP1658CJ.class_("Channel", output.FloatOutput)
CONF_BP1658CJ_ID = "bp1658cj_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(CONF_BP1658CJ_ID): cv.use_id(BP1658CJ),
cv.Required(CONF_ID): cv.declare_id(Channel),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4),
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_BP1658CJ_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
await output.sub_float_outputs(config, parent=config[CONF_BP1658CJ_ID]).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
+6 -13
View File
@@ -48,15 +48,14 @@ void DAC7678Output::dump_config() {
}
}
void DAC7678Output::register_channel(DAC7678Channel *channel) {
auto c = channel->channel_;
this->min_channel_ = std::min(this->min_channel_, c);
this->max_channel_ = std::max(this->max_channel_, c);
channel->set_parent(this);
ESP_LOGV(TAG, "Registered channel: %01u", channel->channel_);
void DAC7678Output::register_channel(uint8_t channel) {
this->min_channel_ = std::min(this->min_channel_, channel);
this->max_channel_ = std::max(this->max_channel_, channel);
ESP_LOGV(TAG, "Registered channel: %01u", channel);
}
void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) {
void DAC7678Output::write_channel(uint8_t channel, float state) {
auto value = static_cast<uint16_t>(roundf(state * 0xFFF));
if (channel >= std::size(this->dac_input_reg_))
return;
if (this->dac_input_reg_[channel] != value) {
@@ -71,10 +70,4 @@ void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) {
this->status_clear_warning();
}
void DAC7678Channel::write_state(float state) {
const float input_rounded = roundf(state * this->full_scale_);
auto input = static_cast<uint16_t>(input_rounded);
this->parent_->set_channel_value_(this->channel_, input);
}
} // namespace esphome::dac7678
+2 -23
View File
@@ -1,34 +1,17 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::dac7678 {
class DAC7678Output;
class DAC7678Channel final : public output::FloatOutput, public Parented<DAC7678Output> {
public:
void set_channel(uint8_t channel) { channel_ = channel; }
protected:
friend class DAC7678Output;
const uint16_t full_scale_ = 0xFFF;
void write_state(float state) override;
uint8_t channel_;
};
/// DAC7678 float output component.
class DAC7678Output final : public Component, public i2c::I2CDevice {
public:
DAC7678Output() {}
void register_channel(DAC7678Channel *channel);
void register_channel(uint8_t channel);
void write_channel(uint8_t channel, float state);
void set_internal_reference(const bool value) { this->internal_reference_ = value; }
@@ -37,12 +20,8 @@ class DAC7678Output final : public Component, public i2c::I2CDevice {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
protected:
friend DAC7678Channel;
bool internal_reference_;
void set_channel_value_(uint8_t channel, uint16_t value);
uint8_t min_channel_{0xFF};
uint8_t max_channel_{0x00};
uint16_t dac_input_reg_[8] = {
+8 -11
View File
@@ -2,29 +2,26 @@ import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
from . import DAC7678Output, dac7678_ns
from . import DAC7678Output
DEPENDENCIES = ["dac7678"]
DAC7678Channel = dac7678_ns.class_("DAC7678Channel", output.FloatOutput)
CONF_DAC7678_ID = "dac7678_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(DAC7678Channel),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.GenerateID(CONF_DAC7678_ID): cv.use_id(DAC7678Output),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7),
}
)
async def to_code(config: ConfigType) -> MockObj:
paren = await cg.get_variable(config[CONF_DAC7678_ID])
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
cg.add(paren.register_channel(var))
await output.register_output(var, config)
return var
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DAC7678_ID])
await output.sub_float_outputs(config, parent=hub).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
cg.add(hub.register_channel(config[CONF_CHANNEL]))
@@ -1,23 +1,21 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
from .. import CONF_EMC2101_ID, EMC2101_COMPONENT_SCHEMA, emc2101_ns
from .. import CONF_EMC2101_ID, EMC2101_COMPONENT_SCHEMA
DEPENDENCIES = ["emc2101"]
EMC2101Output = emc2101_ns.class_("EMC2101Output", output.FloatOutput)
CONFIG_SCHEMA = EMC2101_COMPONENT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(EMC2101Output),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
}
)
async def to_code(config: ConfigType) -> None:
paren = await cg.get_variable(config[CONF_EMC2101_ID])
var = cg.new_Pvariable(config[CONF_ID], paren)
await output.register_output(var, config)
await output.sub_float_outputs(config, parent=config[CONF_EMC2101_ID]).create(
config, None, apply="set_duty_cycle(state)"
)
@@ -1,7 +0,0 @@
#include "emc2101_output.h"
namespace esphome::emc2101 {
void EMC2101Output::write_state(float state) { this->parent_->set_duty_cycle(state); }
} // namespace esphome::emc2101
@@ -1,20 +0,0 @@
#pragma once
#include "../emc2101.h"
#include "esphome/components/output/float_output.h"
namespace esphome::emc2101 {
/// This class allows to control the EMC2101 output.
class EMC2101Output final : public output::FloatOutput {
public:
EMC2101Output(Emc2101Component *parent) : parent_(parent) {}
protected:
/** Used by ESPHome framework. */
void write_state(float state) override;
Emc2101Component *parent_;
};
} // namespace esphome::emc2101
+8 -16
View File
@@ -1,31 +1,23 @@
import esphome.codegen as cg
from esphome.components import i2c, output
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
from esphome.const import CONF_CHANNEL
from esphome.types import ConfigType
from .. import CONF_GP8403_ID, GP8403Component, gp8403_ns
from .. import CONF_GP8403_ID, GP8403Component
DEPENDENCIES = ["gp8403"]
GP8403Output = gp8403_ns.class_(
"GP8403Output", cg.Component, i2c.I2CDevice, output.FloatOutput
)
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(GP8403Output),
cv.GenerateID(): cv.declare_id(output.ApplyFloatOutput),
cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403Component),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=1),
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_GP8403_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
await output.sub_float_outputs(config, parent=config[CONF_GP8403_ID]).create(
config, None, config[CONF_CHANNEL], apply="write_state(state, index)"
)
@@ -1,18 +0,0 @@
#include "gp8403_output.h"
#include "esphome/core/log.h"
namespace esphome::gp8403 {
ESPHOME_LOG_TAG(TAG, "gp8403.output");
void GP8403Output::dump_config() {
ESP_LOGCONFIG(TAG,
"GP8403 Output:\n"
" Channel: %u",
this->channel_);
}
void GP8403Output::write_state(float state) { this->parent_->write_state(state, this->channel_); }
} // namespace esphome::gp8403
@@ -1,21 +0,0 @@
#pragma once
#include "esphome/components/output/float_output.h"
#include "esphome/core/component.h"
#include "esphome/components/gp8403/gp8403.h"
namespace esphome::gp8403 {
class GP8403Output final : public Component, public output::FloatOutput, public Parented<GP8403Component> {
public:
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA - 1; }
void set_channel(uint8_t channel) { this->channel_ = channel; }
void write_state(float state) override;
protected:
uint8_t channel_;
};
} // namespace esphome::gp8403
+3 -8
View File
@@ -70,16 +70,11 @@ void LP5562Output::dump_config() {
LOG_I2C_DEVICE(this);
}
void LP5562Output::set_channel_value_(uint8_t channel, uint8_t value) {
void LP5562Output::write_channel(uint8_t channel, float state) {
if (this->is_failed())
return;
this->status_comm_result(this->write_byte(progmem_read_byte(&LP5562_REG_PWM[channel]), value));
}
void LP5562Channel::write_state(float state) {
const uint8_t max_duty = 255;
const auto duty = static_cast<uint8_t>(roundf(state * max_duty));
this->parent_->set_channel_value_(this->channel_, duty);
const auto duty = static_cast<uint8_t>(roundf(state * 255));
this->status_comm_result(this->write_byte(progmem_read_byte(&LP5562_REG_PWM[channel]), duty));
}
} // namespace esphome::lp5562
+1 -18
View File
@@ -3,24 +3,10 @@
#include <array>
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/output/float_output.h"
namespace esphome::lp5562 {
class LP5562Output;
class LP5562Channel final : public output::FloatOutput, public Parented<LP5562Output> {
public:
void set_channel(uint8_t channel) { this->channel_ = channel; }
protected:
void write_state(float state) override;
uint8_t channel_{0};
};
/// LP5562 4-channel (RGBW) I2C LED driver.
class LP5562Output final : public Component, public i2c::I2CDevice {
public:
@@ -31,12 +17,9 @@ class LP5562Output final : public Component, public i2c::I2CDevice {
void set_logarithmic_dimming(bool logarithmic_dimming) { this->logarithmic_dimming_ = logarithmic_dimming; }
/// Current limit of a channel in steps of 0.1 mA, written to the chip in setup().
void set_channel_current(uint8_t channel, uint8_t current) { this->currents_[channel] = current; }
void write_channel(uint8_t channel, float state);
protected:
friend class LP5562Channel;
void set_channel_value_(uint8_t channel, uint8_t value);
// 17.5 mA, the chip's reset value
std::array<uint8_t, 4> currents_{175, 175, 175, 175};
bool logarithmic_dimming_{false};
+8 -9
View File
@@ -5,11 +5,10 @@ from esphome.const import CONF_CHANNEL, CONF_CURRENT, CONF_ID
import esphome.final_validate as fv
from esphome.types import ConfigType
from . import LP5562Output, lp5562_ns
from . import LP5562Output
DEPENDENCIES = ["lp5562"]
LP5562Channel = lp5562_ns.class_("LP5562Channel", output.FloatOutput)
CONF_LP5562_ID = "lp5562_id"
# Chip reset value of the current registers, 17.5 mA; matches currents_ in lp5562_output.h
CURRENT_RESET_TENTHS = 175
@@ -38,7 +37,7 @@ def _validate_current(amps: float) -> float:
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(LP5562Channel),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.GenerateID(CONF_LP5562_ID): cv.use_id(LP5562Output),
cv.Required(CONF_CHANNEL): cv.enum(LP5562_CHANNELS, lower=True),
cv.Optional(CONF_CURRENT, default="17.5mA"): cv.All(
@@ -65,11 +64,11 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_parented(var, config[CONF_LP5562_ID])
parent = await cg.get_variable(config[CONF_LP5562_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
channel = config[CONF_CHANNEL]
hub = await cg.get_variable(config[CONF_LP5562_ID])
await output.sub_float_outputs(config, parent=hub).create(
config, None, channel, apply="write_channel(index, state)"
)
# The hub already holds the chip's reset value, 17.5 mA
if (current := _tenths(config[CONF_CURRENT])) != CURRENT_RESET_TENTHS:
cg.add(parent.set_channel_current(config[CONF_CHANNEL], current))
await output.register_output(var, config)
cg.add(hub.set_channel_current(channel, current))
+9 -28
View File
@@ -39,19 +39,10 @@ void MCP4728Component::loop() {
}
}
void MCP4728Component::set_channel_value_(MCP4728ChannelIdx channel, uint16_t value) {
uint8_t cn = 0;
if (channel == MCP4728_CHANNEL_A) {
cn = 'A';
} else if (channel == MCP4728_CHANNEL_B) {
cn = 'B';
} else if (channel == MCP4728_CHANNEL_C) {
cn = 'C';
} else {
cn = 'D';
}
ESP_LOGV(TAG, "Setting MCP4728 channel %c to %d!", cn, value);
reg_[channel].data = value;
void MCP4728Component::write_channel(uint8_t channel, float state) {
auto value = static_cast<uint16_t>(roundf(state * 4095));
ESP_LOGV(TAG, "Setting MCP4728 channel %c to %d!", 'A' + channel, value);
this->reg_[channel].data = value;
this->update_ = true;
}
@@ -87,21 +78,11 @@ bool MCP4728Component::seq_write_() {
return err == i2c::ERROR_OK;
}
void MCP4728Component::select_vref_(MCP4728ChannelIdx channel, MCP4728Vref vref) {
reg_[channel].vref = vref;
this->update_ = true;
}
void MCP4728Component::select_power_down_(MCP4728ChannelIdx channel, MCP4728PwrDown pd) {
reg_[channel].pd = pd;
this->update_ = true;
}
void MCP4728Component::select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain) {
reg_[channel].gain = gain;
void MCP4728Component::configure_channel(MCP4728ChannelIdx channel, MCP4728Vref vref, MCP4728Gain gain,
MCP4728PwrDown pd) {
this->reg_[channel].vref = vref;
this->reg_[channel].pd = pd;
this->reg_[channel].gain = gain;
this->update_ = true;
}
+3 -7
View File
@@ -35,8 +35,6 @@ struct DACInputData {
uint16_t data;
};
class MCP4728Channel;
/// MCP4728 float output component.
class MCP4728Component final : public Component, public i2c::I2CDevice {
public:
@@ -47,14 +45,12 @@ class MCP4728Component final : public Component, public i2c::I2CDevice {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void loop() override;
void configure_channel(MCP4728ChannelIdx channel, MCP4728Vref vref, MCP4728Gain gain, MCP4728PwrDown pd);
void write_channel(uint8_t channel, float state);
protected:
friend MCP4728Channel;
void set_channel_value_(MCP4728ChannelIdx channel, uint16_t value);
bool multi_write_();
bool seq_write_();
void select_vref_(MCP4728ChannelIdx channel, MCP4728Vref vref);
void select_power_down_(MCP4728ChannelIdx channel, MCP4728PwrDown pd);
void select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain);
private:
DACInputData reg_[4]{};
+13 -11
View File
@@ -8,7 +8,6 @@ from .. import CONF_MCP4728_ID, MCP4728Component, mcp4728_ns
DEPENDENCIES = ["mcp4728"]
MCP4728Channel = mcp4728_ns.class_("MCP4728Channel", output.FloatOutput)
CONF_VREF = "vref"
CONF_POWER_DOWN = "power_down"
@@ -36,7 +35,7 @@ CHANNEL_OPTIONS = MCP4728ChannelIdx.options(
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(MCP4728Channel),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.GenerateID(CONF_MCP4728_ID): cv.use_id(MCP4728Component),
cv.Required(CONF_CHANNEL): cv.enum(CHANNEL_OPTIONS, upper=True),
cv.Optional(CONF_VREF, default="vdd"): cv.enum(VREF_OPTIONS, upper=False),
@@ -49,13 +48,16 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
paren = await cg.get_variable(config[CONF_MCP4728_ID])
var = cg.new_Pvariable(
config[CONF_ID],
paren,
config[CONF_CHANNEL],
config[CONF_VREF],
config[CONF_GAIN],
config[CONF_POWER_DOWN],
channel = config[CONF_CHANNEL]
hub = await cg.get_variable(config[CONF_MCP4728_ID])
await output.sub_float_outputs(config, parent=hub).create(
config, None, channel, apply="write_channel(index, state)"
)
cg.add(
hub.configure_channel(
channel,
config[CONF_VREF],
config[CONF_GAIN],
config[CONF_POWER_DOWN],
)
)
await output.register_output(var, config)
@@ -1,15 +0,0 @@
#include "mcp4728_output.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::mcp4728 {
void MCP4728Channel::write_state(float state) {
const uint16_t max_duty = 4095;
const float duty_rounded = roundf(state * max_duty);
auto duty = static_cast<uint16_t>(duty_rounded);
this->parent_->set_channel_value_(this->channel_, duty);
}
} // namespace esphome::mcp4728
@@ -1,30 +0,0 @@
#pragma once
#include "../mcp4728.h"
#include "esphome/core/component.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::mcp4728 {
class MCP4728Channel final : public output::FloatOutput {
public:
MCP4728Channel(MCP4728Component *parent, MCP4728ChannelIdx channel, MCP4728Vref vref, MCP4728Gain gain,
MCP4728PwrDown pwrdown)
: parent_(parent), channel_(channel) {
// update VREF
parent->select_vref_(channel, vref);
// update PD
parent->select_power_down_(channel, pwrdown);
// update GAIN
parent->select_gain_(channel, gain);
}
protected:
void write_state(float state) override;
MCP4728Component *parent_;
MCP4728ChannelIdx channel_;
};
} // namespace esphome::mcp4728
+2 -1
View File
@@ -80,7 +80,8 @@ void MY9231OutputComponent::loop() {
}
this->update_ = false;
}
void MY9231OutputComponent::set_channel_value_(uint16_t channel, uint16_t value) {
void MY9231OutputComponent::write_channel(uint16_t channel, float state) {
auto value = uint16_t(state * this->get_max_amount_());
ESP_LOGV(TAG, "set channels %u to %u", channel, value);
uint16_t index = this->num_channels_ - channel - 1;
if (this->pwm_amounts_[index] != value) {
+1 -17
View File
@@ -2,7 +2,6 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/output/float_output.h"
#include <vector>
namespace esphome::my9231 {
@@ -10,7 +9,6 @@ namespace esphome::my9231 {
/// MY9231 float output component.
class MY9231OutputComponent final : public Component {
public:
class Channel;
void set_pin_di(GPIOPin *pin_di) { pin_di_ = pin_di; }
void set_pin_dcki(GPIOPin *pin_dcki) { pin_dcki_ = pin_dcki; }
@@ -26,25 +24,11 @@ class MY9231OutputComponent final : public Component {
/// Send new values if they were updated.
void loop() override;
class Channel final : public output::FloatOutput {
public:
void set_parent(MY9231OutputComponent *parent) { parent_ = parent; }
void set_channel(uint16_t channel) { channel_ = channel; }
protected:
void write_state(float state) override {
auto amount = uint16_t(state * this->parent_->get_max_amount_());
this->parent_->set_channel_value_(this->channel_, amount);
}
MY9231OutputComponent *parent_;
uint16_t channel_;
};
void write_channel(uint16_t channel, float state);
protected:
uint16_t get_max_amount_() const { return (uint32_t(1) << this->bit_depth_) - 1; }
void set_channel_value_(uint16_t channel, uint16_t value);
void init_chips_(uint8_t command);
void write_word_(uint16_t value, uint8_t bits);
void send_di_pulses_(uint8_t count);
+5 -9
View File
@@ -1,4 +1,3 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
@@ -8,21 +7,18 @@ from . import MY9231OutputComponent
DEPENDENCIES = ["my9231"]
Channel = MY9231OutputComponent.class_("Channel", output.FloatOutput)
CONF_MY9231_ID = "my9231_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(CONF_MY9231_ID): cv.use_id(MY9231OutputComponent),
cv.Required(CONF_ID): cv.declare_id(Channel),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.Required(CONF_CHANNEL): cv.uint16_t,
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_MY9231_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
await output.sub_float_outputs(config, parent=config[CONF_MY9231_ID]).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
+38 -1
View File
@@ -13,7 +13,9 @@ from esphome.const import (
CONF_MIN_POWER,
CONF_POWER_SUPPLY,
)
from esphome.core import CORE
from esphome.core import CORE, ID
from esphome.core.entity_helpers import ApplyEntity, SubEntities
from esphome.cpp_generator import MockObj, SafeExpType
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
@@ -54,6 +56,8 @@ BinaryOutput = output_ns.class_("BinaryOutput")
BinaryOutputPtr = BinaryOutput.operator("ptr")
FloatOutput = output_ns.class_("FloatOutput", BinaryOutput)
FloatOutputPtr = FloatOutput.operator("ptr")
ApplyBinaryOutput = output_ns.class_("ApplyBinaryOutput", BinaryOutput)
ApplyFloatOutput = output_ns.class_("ApplyFloatOutput", FloatOutput)
async def setup_output_platform_(obj, config):
@@ -84,6 +88,39 @@ async def register_output(var, config):
await setup_output_platform_(var, config)
async def new_output(config: ConfigType, *args: SafeExpType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_output(var, config)
return var
BINARY_APPLY_ENTITY = ApplyEntity(
ApplyBinaryOutput,
((BinaryOutputPtr, "entity"), (cg.uint16, "index"), (cg.bool_, "state")),
)
FLOAT_APPLY_ENTITY = ApplyEntity(
ApplyFloatOutput,
((FloatOutputPtr, "entity"), (cg.uint16, "index"), (cg.float_, "state")),
)
def sub_binary_outputs(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Return a SubEntities bound to new_output; ``apply`` can use ``entity``, ``index`` and ``state``.
``index`` is the constructor argument (default 0), so a hub's channels share one class.
"""
return SubEntities(new_output, config, parent, BINARY_APPLY_ENTITY)
def sub_float_outputs(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Like sub_binary_outputs; ``apply`` gets the scaled level as ``state``."""
return SubEntities(new_output, config, parent, FLOAT_APPLY_ENTITY)
BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(BinaryOutput),
+12
View File
@@ -68,4 +68,16 @@ class BinaryOutput {
#endif
};
/// Calls the codegen-generated Fn(this, index, state) on a state write; the hub is baked into Fn.
/// The index (a hub's channel) is data, so all channels of a hub share one instantiation.
template<auto Fn> class ApplyBinaryOutput final : public BinaryOutput {
public:
explicit ApplyBinaryOutput(uint16_t index = 0) : index_(index) {}
protected:
void write_state(bool state) override { Fn(this, this->index_, state); }
uint16_t index_; // Fits in the base's tail padding
};
} // namespace esphome::output
+12
View File
@@ -130,4 +130,16 @@ class FloatOutput : public BinaryOutput {
#endif
};
/// Calls the codegen-generated Fn(this, index, state) with the scaled, inverted level; the hub is baked into Fn.
/// The index (a hub's channel) is data, so all channels of a hub share one instantiation.
template<auto Fn> class ApplyFloatOutput final : public FloatOutput {
public:
explicit ApplyFloatOutput(uint16_t index = 0) : index_(index) {}
protected:
void write_state(float state) override { Fn(this, this->index_, state); }
uint16_t index_; // Fits in the base's tail padding
};
} // namespace esphome::output
+7 -8
View File
@@ -4,16 +4,15 @@ import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
from esphome.types import ConfigType
from . import PCA9685Output, pca9685_ns
from . import PCA9685Output
DEPENDENCIES = ["pca9685"]
PCA9685Channel = pca9685_ns.class_("PCA9685Channel", output.FloatOutput)
CONF_PCA9685_ID = "pca9685_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(PCA9685Channel),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.GenerateID(CONF_PCA9685_ID): cv.use_id(PCA9685Output),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=15),
}
@@ -21,8 +20,8 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
paren = await cg.get_variable(config[CONF_PCA9685_ID])
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
cg.add(paren.register_channel(var))
await output.register_output(var, config)
hub = await cg.get_variable(config[CONF_PCA9685_ID])
await output.sub_float_outputs(config, parent=hub).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
cg.add(hub.register_channel(config[CONF_CHANNEL]))
+8 -10
View File
@@ -140,18 +140,16 @@ void PCA9685Output::loop() {
this->update_ = false;
}
void PCA9685Output::register_channel(PCA9685Channel *channel) {
auto c = channel->channel_;
this->min_channel_ = std::min(this->min_channel_, c);
this->max_channel_ = std::max(this->max_channel_, c);
channel->set_parent(this);
void PCA9685Output::register_channel(uint8_t channel) {
this->min_channel_ = std::min(this->min_channel_, channel);
this->max_channel_ = std::max(this->max_channel_, channel);
}
void PCA9685Channel::write_state(float state) {
const uint16_t max_duty = 4096;
const float duty_rounded = roundf(state * max_duty);
auto duty = static_cast<uint16_t>(duty_rounded);
this->parent_->set_channel_value_(this->channel_, duty);
void PCA9685Output::write_channel(uint8_t channel, float state) {
auto value = static_cast<uint16_t>(roundf(state * 4096));
if (this->pwm_amounts_[channel] != value)
this->update_ = true;
this->pwm_amounts_[channel] = value;
}
} // namespace esphome::pca9685
+2 -26
View File
@@ -1,7 +1,6 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::pca9685 {
@@ -22,28 +21,13 @@ inline constexpr uint8_t PCA9685_MODE_OUTNE_HIGHZ = 0x02;
/// Similarly, sets channel output to high if in totem-pole mode, otherwise
inline constexpr uint8_t PCA9685_MODE_OUTNE_LOW = 0x01;
class PCA9685Output;
class PCA9685Channel final : public output::FloatOutput {
public:
void set_channel(uint8_t channel) { channel_ = channel; }
void set_parent(PCA9685Output *parent) { parent_ = parent; }
protected:
friend class PCA9685Output;
void write_state(float state) override;
uint8_t channel_;
PCA9685Output *parent_;
};
/// PCA9685 float output component.
class PCA9685Output final : public Component, public i2c::I2CDevice {
public:
PCA9685Output(uint8_t mode = PCA9685_MODE_OUTPUT_ONACK | PCA9685_MODE_OUTPUT_TOTEM_POLE) : mode_(mode) {}
void register_channel(PCA9685Channel *channel);
void register_channel(uint8_t channel);
void write_channel(uint8_t channel, float state);
void setup() override;
void dump_config() override;
@@ -56,14 +40,6 @@ class PCA9685Output final : public Component, public i2c::I2CDevice {
protected:
/// Configure the chip; false on an I2C failure.
bool setup_registers_();
friend PCA9685Channel;
void set_channel_value_(uint8_t channel, uint16_t value) {
if (this->pwm_amounts_[channel] != value)
this->update_ = true;
this->pwm_amounts_[channel] = value;
}
float frequency_;
uint8_t mode_;
bool extclk_ = false;
@@ -80,7 +80,8 @@ void Sm10BitBase::loop() {
this->update_ = false;
}
void Sm10BitBase::set_channel_value_(uint8_t channel, uint16_t value) {
void Sm10BitBase::write_channel(uint8_t channel, float state) {
auto value = static_cast<uint16_t>(state * 0x3FF);
if (this->pwm_amounts_[channel] != value) {
this->update_ = true;
this->update_channel_ = channel;
+1 -18
View File
@@ -2,15 +2,12 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/output/float_output.h"
#include <vector>
namespace esphome::sm10bit_base {
class Sm10BitBase : public Component {
public:
class Channel;
void set_model(uint8_t model_id) { model_id_ = model_id; }
void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; }
void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; }
@@ -27,23 +24,9 @@ class Sm10BitBase : public Component {
void dump_config() override;
void loop() override;
class Channel final : public output::FloatOutput {
public:
void set_parent(Sm10BitBase *parent) { parent_ = parent; }
void set_channel(uint8_t channel) { channel_ = channel; }
protected:
void write_state(float state) override {
auto amount = static_cast<uint16_t>(state * 0x3FF);
this->parent_->set_channel_value_(this->channel_, amount);
}
Sm10BitBase *parent_;
uint8_t channel_;
};
void write_channel(uint8_t channel, float state);
protected:
void set_channel_value_(uint8_t channel, uint16_t value);
void write_bit_(bool value);
void write_byte_(uint8_t data);
void write_buffer_(uint8_t *buffer, uint8_t size);
+5 -9
View File
@@ -1,4 +1,3 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
@@ -8,21 +7,18 @@ from . import SM16716
DEPENDENCIES = ["sm16716"]
Channel = SM16716.class_("Channel", output.FloatOutput)
CONF_SM16716_ID = "sm16716_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(CONF_SM16716_ID): cv.use_id(SM16716),
cv.Required(CONF_ID): cv.declare_id(Channel),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=254),
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_SM16716_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
await output.sub_float_outputs(config, parent=config[CONF_SM16716_ID]).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
+4 -20
View File
@@ -2,15 +2,12 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/output/float_output.h"
#include <vector>
namespace esphome::sm16716 {
class SM16716 final : public Component {
public:
class Channel;
void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; }
void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; }
void set_num_channels(uint8_t num_channels) { num_channels_ = num_channels; }
@@ -25,29 +22,16 @@ class SM16716 final : public Component {
/// Send new values if they were updated.
void loop() override;
class Channel final : public output::FloatOutput {
public:
void set_parent(SM16716 *parent) { parent_ = parent; }
void set_channel(uint8_t channel) { channel_ = channel; }
protected:
void write_state(float state) override {
auto amount = uint8_t(state * 0xFF);
this->parent_->set_channel_value_(this->channel_, amount);
}
SM16716 *parent_;
uint8_t channel_;
};
protected:
void set_channel_value_(uint8_t channel, uint8_t value) {
void write_channel(uint8_t channel, float state) {
auto value = uint8_t(state * 0xFF);
uint8_t index = this->num_channels_ - channel - 1;
if (this->pwm_amounts_[index] != value) {
this->update_ = true;
}
this->pwm_amounts_[index] = value;
}
protected:
void write_bit_(bool value) {
this->data_pin_->digital_write(value);
this->clock_pin_->digital_write(true);
+5 -9
View File
@@ -1,4 +1,3 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
@@ -9,21 +8,18 @@ from . import SM2135
DEPENDENCIES = ["sm2135"]
CODEOWNERS = ["@BoukeHaarsma23"]
Channel = SM2135.class_("Channel", output.FloatOutput)
CONF_SM2135_ID = "sm2135_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(CONF_SM2135_ID): cv.use_id(SM2135),
cv.Required(CONF_ID): cv.declare_id(Channel),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4),
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_SM2135_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
await output.sub_float_outputs(config, parent=config[CONF_SM2135_ID]).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
+2 -1
View File
@@ -130,7 +130,8 @@ void SM2135::loop() {
this->update_ = false;
}
void SM2135::set_channel_value_(uint8_t channel, uint8_t value) {
void SM2135::write_channel(uint8_t channel, float state) {
auto value = static_cast<uint8_t>(state * 0xff);
if (this->pwm_amounts_[channel] != value) {
this->update_ = true;
this->update_channel_ = channel;
+1 -18
View File
@@ -1,7 +1,6 @@
#pragma once
#include <vector>
#include "esphome/components/output/float_output.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
@@ -23,8 +22,6 @@ enum SM2135Current : uint8_t {
class SM2135 final : public Component {
public:
class Channel;
void set_data_pin(GPIOPin *data_pin) { this->data_pin_ = data_pin; }
void set_clock_pin(GPIOPin *clock_pin) { this->clock_pin_ = clock_pin; }
@@ -49,23 +46,9 @@ class SM2135 final : public Component {
/// Send new values if they were updated.
void loop() override;
class Channel final : public output::FloatOutput {
public:
void set_parent(SM2135 *parent) { parent_ = parent; }
void set_channel(uint8_t channel) { channel_ = channel; }
protected:
void write_state(float state) override {
auto amount = static_cast<uint8_t>(state * 0xff);
this->parent_->set_channel_value_(this->channel_, amount);
}
SM2135 *parent_;
uint8_t channel_;
};
void write_channel(uint8_t channel, float state);
protected:
void set_channel_value_(uint8_t channel, uint8_t value);
void sm2135_set_low_(GPIOPin *pin);
void sm2135_set_high_(GPIOPin *pin);
+5 -9
View File
@@ -1,4 +1,3 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
@@ -9,21 +8,18 @@ from . import SM2235
DEPENDENCIES = ["sm2235"]
CODEOWNERS = ["@Cossid"]
Channel = SM2235.class_("Channel", output.FloatOutput)
CONF_SM2235_ID = "sm2235_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(CONF_SM2235_ID): cv.use_id(SM2235),
cv.Required(CONF_ID): cv.declare_id(Channel),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4),
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_SM2235_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
await output.sub_float_outputs(config, parent=config[CONF_SM2235_ID]).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
+5 -9
View File
@@ -1,4 +1,3 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
@@ -9,21 +8,18 @@ from . import SM2335
DEPENDENCIES = ["sm2335"]
CODEOWNERS = ["@Cossid"]
Channel = SM2335.class_("Channel", output.FloatOutput)
CONF_SM2335_ID = "sm2335_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(CONF_SM2335_ID): cv.use_id(SM2335),
cv.Required(CONF_ID): cv.declare_id(Channel),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4),
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_SM2335_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
await output.sub_float_outputs(config, parent=config[CONF_SM2335_ID]).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
+7 -8
View File
@@ -4,16 +4,15 @@ import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
from esphome.types import ConfigType
from . import TLC59208FOutput, tlc59208f_ns
from . import TLC59208FOutput
DEPENDENCIES = ["tlc59208f"]
TLC59208FChannel = tlc59208f_ns.class_("TLC59208FChannel", output.FloatOutput)
CONF_TLC59208F_ID = "tlc59208f_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(TLC59208FChannel),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.GenerateID(CONF_TLC59208F_ID): cv.use_id(TLC59208FOutput),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7),
}
@@ -21,8 +20,8 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
paren = await cg.get_variable(config[CONF_TLC59208F_ID])
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
cg.add(paren.register_channel(var))
await output.register_output(var, config)
hub = await cg.get_variable(config[CONF_TLC59208F_ID])
await output.sub_float_outputs(config, parent=hub).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
cg.add(hub.register_channel(config[CONF_CHANNEL]))
@@ -119,18 +119,16 @@ void TLC59208FOutput::loop() {
this->update_ = false;
}
void TLC59208FOutput::register_channel(TLC59208FChannel *channel) {
auto c = channel->channel_;
this->min_channel_ = std::min(this->min_channel_, c);
this->max_channel_ = std::max(this->max_channel_, c);
channel->set_parent(this);
void TLC59208FOutput::register_channel(uint8_t channel) {
this->min_channel_ = std::min(this->min_channel_, channel);
this->max_channel_ = std::max(this->max_channel_, channel);
}
void TLC59208FChannel::write_state(float state) {
const uint8_t max_duty = 255;
const float duty_rounded = roundf(state * max_duty);
auto duty = static_cast<uint8_t>(duty_rounded);
this->parent_->set_channel_value_(this->channel_, duty);
void TLC59208FOutput::write_channel(uint8_t channel, float state) {
auto duty = static_cast<uint8_t>(roundf(state * 255));
if (this->pwm_amounts_[channel] != duty)
this->update_ = true;
this->pwm_amounts_[channel] = duty;
}
} // namespace esphome::tlc59208f
@@ -1,8 +1,6 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::tlc59208f {
@@ -19,26 +17,13 @@ inline constexpr uint8_t TLC59208F_MODE2_WDT_15MS = (1 << 0);
inline constexpr uint8_t TLC59208F_MODE2_WDT_25MS = (2 << 0);
inline constexpr uint8_t TLC59208F_MODE2_WDT_35MS = (3 << 0);
class TLC59208FOutput;
class TLC59208FChannel final : public output::FloatOutput, public Parented<TLC59208FOutput> {
public:
void set_channel(uint8_t channel) { channel_ = channel; }
protected:
friend class TLC59208FOutput;
void write_state(float state) override;
uint8_t channel_;
};
/// TLC59208F float output component.
class TLC59208FOutput final : public Component, public i2c::I2CDevice {
public:
TLC59208FOutput(uint8_t mode = TLC59208F_MODE2_OCH) : mode_(mode) {}
void register_channel(TLC59208FChannel *channel);
void register_channel(uint8_t channel);
void write_channel(uint8_t channel, float state);
void setup() override;
void dump_config() override;
@@ -46,14 +31,6 @@ class TLC59208FOutput final : public Component, public i2c::I2CDevice {
void loop() override;
protected:
friend TLC59208FChannel;
void set_channel_value_(uint8_t channel, uint8_t value) {
if (this->pwm_amounts_[channel] != value)
this->update_ = true;
this->pwm_amounts_[channel] = value;
}
uint8_t mode_;
uint8_t min_channel_{0xFF};
+6 -11
View File
@@ -1,29 +1,24 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
from esphome.types import ConfigType
from .. import TLC5947, tlc5947_ns
from .. import TLC5947
DEPENDENCIES = ["tlc5947"]
TLC5947Channel = tlc5947_ns.class_(
"TLC5947Channel", output.FloatOutput, cg.Parented.template(TLC5947)
)
CONF_TLC5947_ID = "tlc5947_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(CONF_TLC5947_ID): cv.use_id(TLC5947),
cv.Required(CONF_ID): cv.declare_id(TLC5947Channel),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.Required(CONF_CHANNEL): cv.uint16_t,
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_TLC5947_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
await output.sub_float_outputs(config, parent=config[CONF_TLC5947_ID]).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
@@ -1,10 +0,0 @@
#include "tlc5947_output.h"
namespace esphome::tlc5947 {
void TLC5947Channel::write_state(float state) {
auto amount = static_cast<uint16_t>(state * 0xfff);
this->parent_->set_channel_value(this->channel_, amount);
}
} // namespace esphome::tlc5947
@@ -1,20 +0,0 @@
#pragma once
#include "esphome/core/helpers.h"
#include "esphome/components/output/float_output.h"
#include "../tlc5947.h"
namespace esphome::tlc5947 {
class TLC5947Channel final : public output::FloatOutput, public Parented<TLC5947> {
public:
void set_channel(uint16_t channel) { this->channel_ = channel; }
protected:
void write_state(float state) override;
uint16_t channel_;
};
} // namespace esphome::tlc5947
+4 -1
View File
@@ -3,7 +3,6 @@
// https://www.ti.com/lit/ds/symlink/tlc5947.pdf
#include <vector>
#include "esphome/components/output/float_output.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
@@ -29,6 +28,10 @@ class TLC5947 final : public Component {
void loop() override;
void set_channel_value(uint16_t channel, uint16_t value);
/// Set a channel from an output level, 0.0 to 1.0.
void write_channel(uint16_t channel, float state) {
this->set_channel_value(channel, static_cast<uint16_t>(state * 0xfff));
}
protected:
GPIOPin *data_pin_;
+6 -11
View File
@@ -1,29 +1,24 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
from esphome.types import ConfigType
from .. import TLC5971, tlc5971_ns
from .. import TLC5971
DEPENDENCIES = ["tlc5971"]
TLC5971Channel = tlc5971_ns.class_(
"TLC5971Channel", output.FloatOutput, cg.Parented.template(TLC5971)
)
CONF_TLC5971_ID = "tlc5971_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(CONF_TLC5971_ID): cv.use_id(TLC5971),
cv.Required(CONF_ID): cv.declare_id(TLC5971Channel),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.Required(CONF_CHANNEL): cv.uint16_t,
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_TLC5971_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
await output.sub_float_outputs(config, parent=config[CONF_TLC5971_ID]).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
@@ -1,10 +0,0 @@
#include "tlc5971_output.h"
namespace esphome::tlc5971 {
void TLC5971Channel::write_state(float state) {
auto amount = static_cast<uint16_t>(state * 0xffff);
this->parent_->set_channel_value(this->channel_, amount);
}
} // namespace esphome::tlc5971
@@ -1,20 +0,0 @@
#pragma once
#include "esphome/core/helpers.h"
#include "esphome/components/output/float_output.h"
#include "../tlc5971.h"
namespace esphome::tlc5971 {
class TLC5971Channel final : public output::FloatOutput, public Parented<TLC5971> {
public:
void set_channel(uint16_t channel) { this->channel_ = channel; }
protected:
void write_state(float state) override;
uint16_t channel_;
};
} // namespace esphome::tlc5971
+4 -1
View File
@@ -4,7 +4,6 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/output/float_output.h"
#include <vector>
namespace esphome::tlc5971 {
@@ -27,6 +26,10 @@ class TLC5971 final : public Component {
void loop() override;
void set_channel_value(uint16_t channel, uint16_t value);
/// Set a channel from an output level, 0.0 to 1.0.
void write_channel(uint16_t channel, float state) {
this->set_channel_value(channel, static_cast<uint16_t>(state * 0xffff));
}
protected:
void transfer_(uint8_t send);
+7 -13
View File
@@ -1,26 +1,20 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_LED
from esphome.const import CONF_LED
from esphome.types import ConfigType
from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns
TM1638OutputLed = tm1638_ns.class_("TM1638OutputLed", output.BinaryOutput, cg.Component)
from ..display import CONF_TM1638_ID, TM1638Component
CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(TM1638OutputLed),
cv.GenerateID(): cv.declare_id(output.ApplyBinaryOutput),
cv.GenerateID(CONF_TM1638_ID): cv.use_id(TM1638Component),
cv.Required(CONF_LED): cv.int_range(min=0, max=7),
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
await cg.register_component(var, config)
cg.add(var.set_lednum(config[CONF_LED]))
await cg.add_variable(var.set_tm1638, config[CONF_TM1638_ID])
await output.sub_binary_outputs(config, parent=config[CONF_TM1638_ID]).create(
config, None, config[CONF_LED], apply="set_led(index, state)"
)
@@ -1,15 +0,0 @@
#include "tm1638_output_led.h"
#include "esphome/core/log.h"
namespace esphome::tm1638 {
ESPHOME_LOG_TAG(TAG, "tm1638.led");
void TM1638OutputLed::write_state(bool state) { tm1638_->set_led(led_, state); }
void TM1638OutputLed::dump_config() {
LOG_BINARY_OUTPUT(this);
ESP_LOGCONFIG(TAG, " LED: %d", led_);
}
} // namespace esphome::tm1638
@@ -1,23 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/output/binary_output.h"
#include "../tm1638.h"
namespace esphome::tm1638 {
class TM1638OutputLed final : public output::BinaryOutput, public Component {
public:
void dump_config() override;
void set_tm1638(TM1638Component *tm1638) { tm1638_ = tm1638; }
void set_lednum(int led) { led_ = led; }
protected:
void write_state(bool state) override;
TM1638Component *tm1638_;
int led_;
};
} // namespace esphome::tm1638
@@ -1,22 +1,12 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MAX_VALUE, CONF_MIN_VALUE
from esphome.types import ConfigType
from .. import (
CONF_WAVESHARE_IO_CH32V003_ID,
WaveshareIOCH32V003Component,
waveshare_io_ch32v003_ns,
)
from .. import CONF_WAVESHARE_IO_CH32V003_ID, WaveshareIOCH32V003Component
DEPENDENCIES = ["waveshare_io_ch32v003"]
WaveshareIOCH32V003Output = waveshare_io_ch32v003_ns.class_(
"WaveshareIOCH32V003Output",
output.FloatOutput,
cg.Parented.template(WaveshareIOCH32V003Component),
)
CONF_SAFE_PWM_LEVELS = "safe_pwm_levels"
@@ -47,7 +37,7 @@ CONF_SAFE_PWM_LEVELS_SCHEMA = cv.Schema(
CONFIG_SCHEMA = cv.All(
output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(WaveshareIOCH32V003Output),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.GenerateID(CONF_WAVESHARE_IO_CH32V003_ID): cv.use_id(
WaveshareIOCH32V003Component
),
@@ -59,9 +49,12 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_WAVESHARE_IO_CH32V003_ID])
min_val = config.get(CONF_SAFE_PWM_LEVELS, {}).get(CONF_MIN_VALUE, DUTY_DEFAULT_MIN)
max_val = config.get(CONF_SAFE_PWM_LEVELS, {}).get(CONF_MAX_VALUE, DUTY_DEFAULT_MAX)
cg.add(var.set_pwm_safe_range(min_val, max_val))
await output.sub_float_outputs(
config, parent=config[CONF_WAVESHARE_IO_CH32V003_ID]
).create(
config,
None,
apply=f"write_pwm_level(state, {min_val}, {max_val})",
)
@@ -1,19 +0,0 @@
#include "waveshare_io_ch32v003_output.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome::waveshare_io_ch32v003 {
ESPHOME_LOG_TAG(TAG, "waveshare_io_ch32v003.output");
void WaveshareIOCH32V003Output::write_state(float state) {
uint8_t pwm_value = static_cast<uint8_t>(state * 255.0f);
uint8_t final_pwm_value = std::clamp(pwm_value, this->pwm_min_value_, this->pwm_max_value_);
if (final_pwm_value != pwm_value) {
ESP_LOGVV(TAG, "Clamping PWM value %u to safe range [%u, %u]", pwm_value, this->pwm_min_value_,
this->pwm_max_value_);
}
this->parent_->set_pwm_value(final_pwm_value);
}
} // namespace esphome::waveshare_io_ch32v003
@@ -1,22 +0,0 @@
#pragma once
#include "../waveshare_io_ch32v003.h"
#include "esphome/components/output/float_output.h"
namespace esphome::waveshare_io_ch32v003 {
class WaveshareIOCH32V003Output : public output::FloatOutput, public Parented<WaveshareIOCH32V003Component> {
public:
void set_pwm_safe_range(uint8_t min_value, uint8_t max_value) {
this->pwm_min_value_ = min_value;
this->pwm_max_value_ = max_value;
}
protected:
void write_state(float state) override;
uint8_t pwm_min_value_{1};
uint8_t pwm_max_value_{247};
};
} // namespace esphome::waveshare_io_ch32v003
@@ -1,5 +1,6 @@
#include "waveshare_io_ch32v003.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome::waveshare_io_ch32v003 {
@@ -73,13 +74,20 @@ void WaveshareIOCH32V003Component::set_pwm_value(uint8_t value) {
if (this->is_failed())
return;
// PWM limits are enforced at the output component level to protect hardware
// based on circuit schematic requirements. This follows the pattern from the
// original Waveshare IO library function "void IO_EXTENSION_Pwm_Output(uint8_t Value)".
// The safe PWM range is enforced by write_pwm_level(), as in the original Waveshare IO
// library function "void IO_EXTENSION_Pwm_Output(uint8_t Value)".
this->status_comm_result(this->write_byte(IO_EXTENSION_PWM_ADDR, value), LOG_STR("Failed to set PWM duty cycle"));
}
void WaveshareIOCH32V003Component::write_pwm_level(float state, uint8_t min_value, uint8_t max_value) {
auto pwm_value = static_cast<uint8_t>(state * 255.0f);
uint8_t final_pwm_value = std::clamp(pwm_value, min_value, max_value);
if (final_pwm_value != pwm_value) {
ESP_LOGVV(TAG, "Clamping PWM value %u to safe range [%u, %u]", pwm_value, min_value, max_value);
}
this->set_pwm_value(final_pwm_value);
}
bool WaveshareIOCH32V003Component::write_gpio_modes_() {
if (this->is_failed())
return false;
@@ -26,6 +26,8 @@ class WaveshareIOCH32V003Component : public Component,
uint16_t get_adc_value();
uint8_t get_rtc_interrupt_status();
void set_pwm_value(uint8_t value); // 0 - 255
/// Write an output level, 0.0 to 1.0, clamped to the board's safe PWM range.
void write_pwm_level(float state, uint8_t min_value, uint8_t max_value);
protected:
bool digital_read_hw(uint8_t pin) override;
+11
View File
@@ -0,0 +1,11 @@
import esphome.codegen as cg
from esphome.types import ConfigType
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# Builds the min/max power scaling so the Apply output tests cover it
async def to_code_testing(config: ConfigType) -> None:
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
manifest.to_code = to_code_testing
@@ -0,0 +1,81 @@
#include <gtest/gtest.h>
#include <vector>
#include "esphome/components/output/binary_output.h"
#include "esphome/components/output/float_output.h"
namespace esphome::output::testing {
static void *last_output = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static uint16_t last_index = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static std::vector<bool> bool_writes; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static std::vector<float> levels; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static void on_binary_write(BinaryOutput *output, uint16_t index, bool state) {
last_output = output;
last_index = index;
bool_writes.push_back(state);
}
static void on_float_write(FloatOutput *output, uint16_t index, float state) {
last_output = output;
last_index = index;
levels.push_back(state);
}
// The index sits in the base's tail padding; only the power supply requester leaves none.
#ifndef USE_POWER_SUPPLY
static_assert(sizeof(ApplyBinaryOutput<on_binary_write>) == sizeof(BinaryOutput));
static_assert(sizeof(ApplyFloatOutput<on_float_write>) == sizeof(FloatOutput));
#endif
static void reset() {
last_output = nullptr;
last_index = 0;
bool_writes.clear();
levels.clear();
}
TEST(ApplyBinaryOutput, WritesReachTheFunctionWithIndexAndInversion) {
reset();
ApplyBinaryOutput<on_binary_write> out(7);
out.turn_on();
out.turn_off();
out.set_inverted(true);
out.turn_on();
EXPECT_EQ(bool_writes, (std::vector<bool>{true, false, false}));
EXPECT_EQ(last_output, &out);
EXPECT_EQ(last_index, 7);
}
TEST(ApplyFloatOutput, WritesReachTheFunctionWithIndexInversionAndClamp) {
reset();
ApplyFloatOutput<on_float_write> out(3);
out.set_level(0.25f);
out.turn_on();
out.set_inverted(true);
out.set_level(0.25f);
out.set_level(2.0f);
EXPECT_EQ(levels, (std::vector<float>{0.25f, 1.0f, 0.75f, 0.0f}));
EXPECT_EQ(last_output, &out);
EXPECT_EQ(last_index, 3);
}
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
TEST(ApplyFloatOutput, PowerScalingIsAppliedBeforeTheFunction) {
reset();
ApplyFloatOutput<on_float_write> out;
out.set_min_power(0.2f);
out.set_max_power(0.8f);
out.set_zero_means_zero(true);
out.set_level(0.5f);
out.turn_off();
ASSERT_EQ(levels.size(), 2u);
EXPECT_FLOAT_EQ(levels[0], 0.5f);
EXPECT_EQ(levels[1], 0.0f);
EXPECT_EQ(last_index, 0);
}
#endif
} // namespace esphome::output::testing
+37 -1
View File
@@ -8,7 +8,7 @@ from typing import Any
import pytest
import esphome.codegen as cg
from esphome.components import button, number, select, switch
from esphome.components import button, number, output, select, switch
from esphome.config_validation import Invalid
from esphome.const import (
CONF_DEVICE_CLASS,
@@ -1464,6 +1464,30 @@ async def test_sub_entities_apply_shares_equal_functions(
]
@pytest.mark.asyncio
async def test_sub_outputs_share_one_function_per_hub(
setup_test_environment: list[str], apply_functions: list[tuple[str, str, str]]
) -> None:
"""A hub's channels pass their channel as the index, so they share one function."""
calls: list[tuple[Any, ...]] = []
cls = output.ApplyFloatOutput
config = {"a": _apply_conf("ch0", cls), "b": _apply_conf("ch5", cls)}
sub = _apply_sub(calls, config, output.FLOAT_APPLY_ENTITY)
await sub("a", None, 0, apply="write_channel(index, state)")
await sub("b", None, 5, apply="write_channel(index, state)")
assert calls == [
("ch0", "<esphome__ch0__fn>", "0"),
("ch5", "<esphome__ch0__fn>", "5"),
]
assert apply_functions == [
(
"esphome__ch0__fn",
"output::FloatOutput * entity, uint16_t index, float state",
"::hub->write_channel(index, state);",
)
]
@pytest.mark.asyncio
async def test_sub_entities_apply_parent_id(
monkeypatch: pytest.MonkeyPatch,
@@ -1565,6 +1589,18 @@ async def test_sub_entities_apply_needs_parent(
"select::Select * entity, size_t index",
"entity->publish_state(index);",
),
(
output.sub_binary_outputs,
output.ApplyBinaryOutput,
"output::BinaryOutput * entity, uint16_t index, bool state",
None,
),
(
output.sub_float_outputs,
output.ApplyFloatOutput,
"output::FloatOutput * entity, uint16_t index, float state",
None,
),
],
)
def test_domain_apply_entities(