Compare commits

..
Author SHA1 Message Date
J. Nick Koston 37e7597791 [veml6040] Zero the channel counts so clang-tidy sees them set 2026-10-22 15:27:47 -10:00
J. Nick Koston 608bad93e8 [dfrobot_sen0609] Use core parse_number<float> 2026-10-22 15:03:17 -10:00
J. Nick Koston 3d293b0cab [esp32_ulp] Give mark_failed a reason 2026-10-22 15:02:39 -10:00
J. Nick Koston 397cad4bb9 [dfrobot_sen0465_base] Use pow10_int and drop the dump_config failure line 2026-10-22 15:02:27 -10:00
J. Nick Koston 8e1498b2c6 [cw2017] Use status_comm_result and SUB_SENSOR 2026-10-22 15:02:00 -10:00
J. Nick Koston d1c42968c1 [veml6040] Use read_byte_16_le, status_comm_result, SUB_SENSOR and options() 2026-10-22 15:01:39 -10:00
J. Nick Koston a56a7603dd Merge remote-tracking branch 'origin/dev' into recent-components-py-helpers 2026-10-22 15:01:09 -10:00
J. Nick Koston 47f1da0a8d [components] Use options() and sub_text_sensors in recently added components
Build the dfrobot_sen0465 alarm types and mics_5524 conversion models with
options(), and register the systa_bus error code text sensor with
sub_text_sensors. The generated C++ is unchanged.
2026-10-17 17:32:55 -10:00
16 changed files with 51 additions and 279 deletions

No files matched your search

+7 -12
View File
@@ -163,26 +163,21 @@ void CW2017::update() {
if (!this->ready_)
return;
bool ok = true;
if (this->battery_level_sensor_ != nullptr) {
uint8_t soc = 0;
if (this->read_byte(REG_SOC, &soc) && soc <= 100) {
ok = this->read_byte(REG_SOC, &soc) && soc <= 100;
if (ok)
this->battery_level_sensor_->publish_state(soc);
} else {
this->status_set_warning();
return;
}
}
if (this->battery_voltage_sensor_ != nullptr) {
if (ok && this->battery_voltage_sensor_ != nullptr) {
uint16_t raw = 0;
if (this->read_byte_16(REG_VCELL, &raw)) {
ok = this->read_byte_16(REG_VCELL, &raw);
if (ok)
this->battery_voltage_sensor_->publish_state((raw & VCELL_MASK) * VCELL_VOLTS_PER_LSB);
} else {
this->status_set_warning();
return;
}
}
this->status_clear_warning();
this->status_comm_result(ok);
}
void CW2017::dump_config() {
+2 -4
View File
@@ -12,8 +12,8 @@ class CW2017 : public PollingComponent, public i2c::I2CDevice {
public:
explicit CW2017(const uint8_t *profile) : profile_(profile) {}
void set_battery_level_sensor(sensor::Sensor *sensor) { this->battery_level_sensor_ = sensor; }
void set_battery_voltage_sensor(sensor::Sensor *sensor) { this->battery_voltage_sensor_ = sensor; }
SUB_SENSOR(battery_level)
SUB_SENSOR(battery_voltage)
void setup() override;
void update() override;
@@ -29,8 +29,6 @@ class CW2017 : public PollingComponent, public i2c::I2CDevice {
void wait_ready_(uint8_t attempt);
const uint8_t *profile_;
sensor::Sensor *battery_level_sensor_{nullptr};
sensor::Sensor *battery_voltage_sensor_{nullptr};
bool ready_{false};
bool restart_pending_{false};
uint8_t init_attempts_{0};
@@ -34,10 +34,7 @@ DFRobotSEN0465Component = dfrobot_sen0465_base_ns.class_(
"DFRobotSEN0465Component", cg.PollingComponent
)
AlarmType = dfrobot_sen0465_base_ns.enum("AlarmType", is_class=True)
ALARM_TYPES = {
"LOW": AlarmType.ALARM_TYPE_LOW,
"HIGH": AlarmType.ALARM_TYPE_HIGH,
}
ALARM_TYPES = AlarmType.options("LOW", "HIGH", prefix="ALARM_TYPE_")
# One piece of the temperature compensation from the DFRobot_MultiGasSensor library:
# for low < T <= high, compensated = raw / (a * T + b) - (c * T + d)
@@ -72,10 +72,7 @@ void DFRobotSEN0465Component::dump_config() {
if (this->alarm_type_ == AlarmType::ALARM_TYPE_OFF) {
ESP_LOGCONFIG(TAG, " Alarm: off");
} else {
float scale = 1.0f;
for (uint8_t i = 0; i < this->alarm_decimals_; i++)
scale *= 10.0f;
const float threshold = this->alarm_threshold_ / scale;
const float threshold = this->alarm_threshold_ / pow10_int(this->alarm_decimals_);
ESP_LOGCONFIG(TAG, " Alarm: %s %.*f",
this->alarm_type_ == AlarmType::ALARM_TYPE_HIGH ? LOG_STR_LITERAL("above") : LOG_STR_LITERAL("below"),
this->alarm_decimals_, threshold);
@@ -83,9 +80,6 @@ void DFRobotSEN0465Component::dump_config() {
if (this->setup_incomplete_) {
ESP_LOGCONFIG(TAG, " Setup incomplete: yes");
}
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Concentration", this->concentration_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
@@ -228,23 +222,14 @@ void DFRobotSEN0465Component::handle_data_(const uint8_t *frame) {
return;
}
float concentration = encode_uint16(frame[2], frame[3]);
switch (frame[5]) {
case 0:
break;
case 1:
concentration *= 0.1f;
break;
case 2:
concentration *= 0.01f;
break;
default:
ESP_LOGW(TAG, "Invalid number of decimal places: %u", frame[5]);
if (this->concentration_sensor_ != nullptr)
this->concentration_sensor_->publish_state(NAN);
this->status_set_warning();
return;
if (frame[5] > 2) {
ESP_LOGW(TAG, "Invalid number of decimal places: %u", frame[5]);
if (this->concentration_sensor_ != nullptr)
this->concentration_sensor_->publish_state(NAN);
this->status_set_warning();
return;
}
float concentration = encode_uint16(frame[2], frame[3]) * pow10_int(-frame[5]);
#ifdef USE_DFROBOT_SEN0465_TEMPERATURE_COMPENSATION
if (!this->compensation_.empty())
@@ -95,12 +95,6 @@ static bool answer_means_done(Operation operation, const char *line) {
}
}
static bool parse_float(const char *str, float &value) {
char *end;
value = strtof(str, &end);
return end != str && *end == '\0';
}
static const LogString *operation_to_str(Operation operation) {
switch (operation) {
case Operation::OPERATION_STOP:
@@ -286,9 +280,6 @@ void DFRobotSEN0609Component::handle_frame_(char *line) {
return;
}
char *fields[FRAME_FIELDS];
float distance;
float speed;
float energy;
if (split_fields(line + 7, fields, FRAME_FIELDS) < FRAME_FIELDS) {
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
return;
@@ -308,7 +299,10 @@ void DFRobotSEN0609Component::handle_frame_(char *line) {
#endif
return;
}
if (!parse_float(fields[2], distance) || !parse_float(fields[3], speed) || !parse_float(fields[4], energy)) {
const optional<float> distance = parse_number<float>(fields[2]);
const optional<float> speed = parse_number<float>(fields[3]);
const optional<float> energy = parse_number<float>(fields[4]);
if (!distance.has_value() || !speed.has_value() || !energy.has_value()) {
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
return;
}
@@ -317,7 +311,7 @@ void DFRobotSEN0609Component::handle_frame_(char *line) {
this->has_target_binary_sensor_->publish_state(true);
#endif
#ifdef USE_SENSOR
this->publish_target_(distance, speed, energy);
this->publish_target_(*distance, *speed, *energy);
#endif
}
-10
View File
@@ -181,16 +181,6 @@ def get_all_display_metadata() -> dict[str, DisplayMetaData]:
return {id_.id: meta for id_, meta in entries}
def config_has_writer(config: ConfigType) -> bool:
"""Return True when the display config draws anything."""
return (
config.get(CONF_AUTO_CLEAR_ENABLED) is True
or config.get(CONF_PAGES) is not None
or config.get(CONF_LAMBDA) is not None
or config.get(CONF_SHOW_TEST_CARD) is True
)
def get_display_metadata(display_id: ID) -> DisplayMetaData:
"""Get display metadata by ID object
@@ -1,28 +0,0 @@
#include "init_sequence.h"
#include "esphome/core/hal.h"
namespace esphome::display {
InitSequenceStep InitSequenceReader::next(InitSequenceEntry &entry, uint8_t max_count) {
const size_t remaining = this->length_ - this->index_;
if (remaining == 0)
return INIT_SEQUENCE_STEP_END;
const uint8_t *pos = this->sequence_ + this->index_;
if (remaining >= 2) {
entry.value = progmem_read_byte(pos);
entry.count = progmem_read_byte(pos + 1);
entry.args = pos + 2;
if (entry.count == INIT_SEQUENCE_DELAY_FLAG) {
this->index_ += 2;
return INIT_SEQUENCE_STEP_DELAY;
}
if (entry.count <= max_count && remaining - 2 >= entry.count) {
this->index_ += 2 + entry.count;
return INIT_SEQUENCE_STEP_COMMAND;
}
}
this->index_ = this->length_;
return INIT_SEQUENCE_STEP_MALFORMED;
}
} // namespace esphome::display
@@ -1,40 +0,0 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace esphome::display {
/// Count byte that marks a delay entry: the byte before it is the delay in ms.
static constexpr uint8_t INIT_SEQUENCE_DELAY_FLAG = 0xFF;
/// Unscoped so `while (auto step = reader.next(entry))` stops at END.
enum InitSequenceStep : uint8_t {
INIT_SEQUENCE_STEP_END = 0,
INIT_SEQUENCE_STEP_COMMAND,
INIT_SEQUENCE_STEP_DELAY,
INIT_SEQUENCE_STEP_MALFORMED,
};
/// `value` is the command or the delay in ms; `args` points into the (possibly PROGMEM) sequence.
struct InitSequenceEntry {
const uint8_t *args;
uint8_t value;
uint8_t count;
};
/// Walks a mipi.flatten_sequence table (`cmd, count, args...` or `ms, 0xFF`) with progmem reads; resumable.
class InitSequenceReader {
public:
InitSequenceReader(const uint8_t *sequence, size_t length) : sequence_(sequence), length_(length) {}
/// Fill `entry` with the next entry; a command with more than `max_count` args is MALFORMED.
InitSequenceStep next(InitSequenceEntry &entry, uint8_t max_count = 0xFE);
protected:
const uint8_t *sequence_;
size_t length_;
size_t index_{0};
};
} // namespace esphome::display
+2 -2
View File
@@ -38,7 +38,7 @@ void ESP32ULP::setup() {
esp_err_t err = ulp_set_wakeup_period(0, this->wakeup_period_us_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "ulp_set_wakeup_period failed: %s", esp_err_to_name(err));
this->mark_failed();
this->mark_failed(LOG_STR("Wakeup period not set"));
return;
}
}
@@ -48,7 +48,7 @@ void ESP32ULP::setup() {
if (esp_reset_reason() == ESP_RST_DEEPSLEEP && s_program_loaded) {
this->loaded_ = true;
} else if (this->run_on_boot_ && !this->load_and_run()) {
this->mark_failed();
this->mark_failed(LOG_STR("Program did not start"));
return;
}
if (this->wakeup_from_sleep_) {
+3 -4
View File
@@ -43,10 +43,9 @@ CONF_RATIO_SENSOR = "ratio_sensor"
CONF_RS_SENSOR = "rs_sensor"
CONF_VOLTAGE_SENSOR = "voltage_sensor"
CONVERSION_MODELS = {
"dfrobot": ConversionModel.CONVERSION_MODEL_DFROBOT,
"datasheet": ConversionModel.CONVERSION_MODEL_DATASHEET,
}
CONVERSION_MODELS = ConversionModel.options(
"dfrobot", "datasheet", prefix="CONVERSION_MODEL_"
)
# Divider guard limits (input max, pin max) in volts by target platform. ESP32 and RP2: VDD and
# the absolute maximum VDD + 0.3 V; a bare ESP8266 A0 measures 0 - 1 V, boards with an onboard
@@ -36,9 +36,9 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if config[CONF_MODEL] == CONF_SYSTASOLAR_AQUA and CONF_ERROR_CODE in config:
sens = await text_sensor.new_text_sensor(config[CONF_ERROR_CODE])
cg.add(var.set_error_code_text_sensor(sens))
# The schema only lets a model's own keys through, so unconfigured ones are skipped here
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_ERROR_CODE, var.set_error_code_text_sensor)
systa_bus = await cg.get_variable(config[CONF_SYSTA_BUS_ID])
await register_systa_bus_listener(systa_bus, var)
+9 -8
View File
@@ -32,14 +32,15 @@ VEML6040Component = veml6040_ns.class_(
)
VEML6040IntegrationTime = veml6040_ns.enum("VEML6040IntegrationTime")
VEML6040_INTEGRATION_TIMES = {
"40ms": VEML6040IntegrationTime.VEML6040_INTEGRATION_TIME_40MS,
"80ms": VEML6040IntegrationTime.VEML6040_INTEGRATION_TIME_80MS,
"160ms": VEML6040IntegrationTime.VEML6040_INTEGRATION_TIME_160MS,
"320ms": VEML6040IntegrationTime.VEML6040_INTEGRATION_TIME_320MS,
"640ms": VEML6040IntegrationTime.VEML6040_INTEGRATION_TIME_640MS,
"1280ms": VEML6040IntegrationTime.VEML6040_INTEGRATION_TIME_1280MS,
}
VEML6040_INTEGRATION_TIMES = VEML6040IntegrationTime.options(
"40ms",
"80ms",
"160ms",
"320ms",
"640ms",
"1280ms",
prefix="VEML6040_INTEGRATION_TIME_",
)
color_channel_schema = sensor.sensor_schema(
icon=ICON_LIGHTBULB,
+3 -15
View File
@@ -1,5 +1,4 @@
#include "veml6040.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -60,25 +59,14 @@ void VEML6040Component::dump_config() {
LOG_SENSOR(" ", "Color temperature", this->color_temperature_sensor_);
}
bool VEML6040Component::read_channel_(uint8_t reg, uint16_t &counts) {
uint8_t data[2];
if (this->read_register(reg, data, sizeof data) != i2c::ERROR_OK)
return false;
counts = encode_uint16(data[1], data[0]);
return true;
}
void VEML6040Component::update() {
if (!this->data_ready_)
return;
uint16_t red, green, blue, white;
if (!this->read_channel_(R_DATA_REG, red) || !this->read_channel_(G_DATA_REG, green) ||
!this->read_channel_(B_DATA_REG, blue) || !this->read_channel_(W_DATA_REG, white)) {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
uint16_t red{0}, green{0}, blue{0}, white{0};
if (!this->status_comm_result(this->read_byte_16_le(R_DATA_REG, &red) && this->read_byte_16_le(G_DATA_REG, &green) &&
this->read_byte_16_le(B_DATA_REG, &blue) && this->read_byte_16_le(W_DATA_REG, &white)))
return;
}
this->status_clear_warning();
ESP_LOGV(TAG, "Counts: red=%u green=%u blue=%u white=%u", red, green, blue, white);
+6 -13
View File
@@ -29,23 +29,16 @@ class VEML6040Component final : public PollingComponent, public i2c::I2CDevice {
void dump_config() override;
void update() override;
void set_red_sensor(sensor::Sensor *sensor) { this->red_sensor_ = sensor; }
void set_green_sensor(sensor::Sensor *sensor) { this->green_sensor_ = sensor; }
void set_blue_sensor(sensor::Sensor *sensor) { this->blue_sensor_ = sensor; }
void set_white_sensor(sensor::Sensor *sensor) { this->white_sensor_ = sensor; }
void set_illuminance_sensor(sensor::Sensor *sensor) { this->illuminance_sensor_ = sensor; }
void set_color_temperature_sensor(sensor::Sensor *sensor) { this->color_temperature_sensor_ = sensor; }
SUB_SENSOR(red)
SUB_SENSOR(green)
SUB_SENSOR(blue)
SUB_SENSOR(white)
SUB_SENSOR(illuminance)
SUB_SENSOR(color_temperature)
protected:
bool read_channel_(uint8_t reg, uint16_t &counts);
uint32_t integration_time_ms_() const { return 40u << this->integration_time_; }
sensor::Sensor *red_sensor_{nullptr};
sensor::Sensor *green_sensor_{nullptr};
sensor::Sensor *blue_sensor_{nullptr};
sensor::Sensor *white_sensor_{nullptr};
sensor::Sensor *illuminance_sensor_{nullptr};
sensor::Sensor *color_temperature_sensor_{nullptr};
const float glass_attenuation_factor_;
const float color_temperature_offset_;
const VEML6040IntegrationTime integration_time_;
@@ -1,82 +0,0 @@
#include <gtest/gtest.h>
#include <cstdint>
#include "esphome/components/display/init_sequence.h"
namespace esphome::display::testing {
TEST(InitSequenceReader, WalksCommandsAndDelays) {
static const uint8_t SEQ[] = {0x11, 0x00, 120, 0xFF, 0x3A, 0x01, 0x55, 0x2A, 0x02, 0x00, 0xEF};
InitSequenceReader reader(SEQ, sizeof(SEQ));
InitSequenceEntry entry{};
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
EXPECT_EQ(entry.value, 0x11);
EXPECT_EQ(entry.count, 0);
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_DELAY);
EXPECT_EQ(entry.value, 120);
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
EXPECT_EQ(entry.value, 0x3A);
ASSERT_EQ(entry.count, 1);
EXPECT_EQ(entry.args, &SEQ[6]);
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
EXPECT_EQ(entry.value, 0x2A);
ASSERT_EQ(entry.count, 2);
EXPECT_EQ(entry.args[0], 0x00);
EXPECT_EQ(entry.args[1], 0xEF);
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
}
TEST(InitSequenceReader, EmptySequenceEnds) {
InitSequenceReader reader(nullptr, 0);
InitSequenceEntry entry{};
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
}
// A count above 127 is a real length, not a flag bit.
TEST(InitSequenceReader, HonoursFullCount) {
uint8_t seq[2 + 253 + 2] = {0x32, 253};
seq[2 + 253] = 0x29;
InitSequenceReader reader(seq, sizeof(seq));
InitSequenceEntry entry{};
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
EXPECT_EQ(entry.value, 0x32);
EXPECT_EQ(entry.count, 253);
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
EXPECT_EQ(entry.value, 0x29);
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
}
TEST(InitSequenceReader, TruncatedHeaderIsMalformed) {
static const uint8_t SEQ[] = {0x11, 0x00, 0x29};
InitSequenceReader reader(SEQ, sizeof(SEQ));
InitSequenceEntry entry{};
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_MALFORMED);
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
}
TEST(InitSequenceReader, ShortArgsAreMalformed) {
static const uint8_t SEQ[] = {0x2A, 0x04, 0x00, 0x00, 0x00};
InitSequenceReader reader(SEQ, sizeof(SEQ));
InitSequenceEntry entry{};
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_MALFORMED);
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
}
TEST(InitSequenceReader, CountOverMaxIsMalformed) {
static const uint8_t SEQ[] = {10, 0xFF, 0x2A, 0x02, 0x00, 0xEF};
InitSequenceReader reader(SEQ, sizeof(SEQ));
InitSequenceEntry entry{};
ASSERT_EQ(reader.next(entry, 1), INIT_SEQUENCE_STEP_DELAY);
EXPECT_EQ(reader.next(entry, 1), INIT_SEQUENCE_STEP_MALFORMED);
}
} // namespace esphome::display::testing
+2 -20
View File
@@ -1,4 +1,4 @@
"""Tests for display.add_writer and display.config_has_writer."""
"""Tests for display.add_writer."""
from unittest.mock import AsyncMock, MagicMock, patch
@@ -6,8 +6,7 @@ import pytest
from esphome import codegen as cg
from esphome.components import display
from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.const import CONF_AUTO_CLEAR_ENABLED, CONF_LAMBDA, CONF_PAGES
from esphome.const import CONF_LAMBDA
@pytest.mark.parametrize(
@@ -40,20 +39,3 @@ async def test_add_writer_without_lambda_does_nothing() -> None:
process.assert_not_awaited()
add.assert_not_called()
@pytest.mark.parametrize(
("config", "expected"),
[
({}, False),
({CONF_AUTO_CLEAR_ENABLED: True}, True),
({CONF_AUTO_CLEAR_ENABLED: False}, False),
({CONF_AUTO_CLEAR_ENABLED: "unspecified"}, False),
({CONF_PAGES: []}, True),
({CONF_LAMBDA: "x"}, True),
({CONF_SHOW_TEST_CARD: True}, True),
({CONF_SHOW_TEST_CARD: False}, False),
],
)
def test_config_has_writer(config: dict, expected: bool) -> None:
assert display.config_has_writer(config) is expected