Compare commits

..

42 Commits

Author SHA1 Message Date
J. Nick Koston
dcab12adae isra 2026-01-25 20:03:44 -10:00
J. Nick Koston
fb714636e3 missed 2026-01-25 20:02:46 -10:00
J. Nick Koston
05a431ea54 fixup 2026-01-25 20:02:46 -10:00
J. Nick Koston
1a34b4e7d7 [api] Remove duplicate peername storage to save RAM 2026-01-25 18:17:47 -10:00
Jonathan Swoboda
011407ea8b Merge branch 'release' into dev 2026-01-25 13:21:39 -05:00
Jonathan Swoboda
1141e83a7c Merge pull request #13529 from esphome/bump-2026.1.2
2026.1.2
2026-01-25 13:21:26 -05:00
Jonathan Swoboda
214ce95cf3 Bump version to 2026.1.2 2026-01-25 12:22:18 -05:00
J. Nick Koston
3a7b83ba93 [wifi] Fix scan flag race condition causing reconnect failure on ESP8266/LibreTiny (#13514) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
cc2f3d85dc [wifi] Fix watchdog timeout on P4 WiFi scan (#13520) 2026-01-25 12:22:18 -05:00
Jonathan Swoboda
723f67d5e2 [i2c] Increase ESP-IDF I2C transaction timeout from 20ms to 100ms (#13483)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:22:18 -05:00
Jonathan Swoboda
70e45706d9 [modbus_controller] Fix YAML serialization error with custom_command (#13482)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:22:18 -05:00
Jas Strong
56a2a2269f [rd03d] Fix speed and resolution field order (#13495)
Co-authored-by: jas <jas@asspa.in>
2026-01-25 12:22:18 -05:00
Keith Burzinski
d6841ba33a [light] Fix cwww state restore (#13493) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
10cbd0164a [lvgl] Fix setting empty text (#13494) 2026-01-25 12:22:18 -05:00
Big Mike
d285706b41 [sen5x] Fix store baseline functionality (#13469) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ef469c20df [slow_pwm] Fix dump_summary deprecation warning (#13460) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
6870d3dc50 [mipi_rgb] Add software reset command to st7701s init sequence (#13470) 2026-01-25 12:22:18 -05:00
Keith Burzinski
9cc39621a6 [ir_rf_proxy] Remove unnecessary headers, add tests (#13464) 2026-01-25 12:22:18 -05:00
J. Nick Koston
c4f7d09553 [rpi_dpi_rgb] Fix dump_summary deprecation warning (#13461) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ab1661ef22 [mipi_rgb] Fix dump_summary deprecation warning (#13463) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ccbf17d5ab [st7701s] Fix dump_summary deprecation warning (#13462) 2026-01-25 12:22:18 -05:00
J. Nick Koston
bac96086be [wifi] Fix scan flag race condition causing reconnect failure on ESP8266/LibreTiny (#13514) 2026-01-25 12:16:07 -05:00
Clyde Stubbs
c32e4bc65b [wifi] Fix watchdog timeout on P4 WiFi scan (#13520) 2026-01-26 03:52:23 +11:00
Douwe
993765d732 [water_heater] Remove Component inheritance from base class (#13510)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 01:18:13 +00:00
Stephen Cox
8d84fe0113 [sy6970] Support for the sy6970 BMS chip (#13311) 2026-01-25 08:31:26 +11:00
Big Mike
58746b737f [sen5x] Eliminate product name string (#13489) 2026-01-24 11:07:12 -10:00
Big Mike
f93e843972 [sen5x] Fix mangled serial number (#13491) 2026-01-24 09:55:51 -10:00
Peter Meiser
60968d311b [thermostat] make comparisons consistent with documentation (#13499) 2026-01-24 00:20:18 -06:00
J. Nick Koston
30584e2e96 [sensirion_common] Use SmallBufferWithHeapFallback helper (#13496) 2026-01-23 22:53:44 -06:00
Jonathan Swoboda
468ae39a9e [i2c] Increase ESP-IDF I2C transaction timeout from 20ms to 100ms (#13483)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 23:13:03 -05:00
Big Mike
beb9c8d328 [sen5x] Fix missing this-> on class members and member functions (#13497) 2026-01-23 17:04:09 -10:00
Jonathan Swoboda
cdda3fb7cc [modbus_controller] Fix YAML serialization error with custom_command (#13482)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 22:01:40 -05:00
Jas Strong
bba00a3906 [rd03d] Fix speed and resolution field order (#13495)
Co-authored-by: jas <jas@asspa.in>
2026-01-23 22:01:19 -05:00
dependabot[bot]
42e50ca178 Bump github/codeql-action from 4.31.10 to 4.31.11 (#13488)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-23 16:26:11 -10:00
Big Mike
165e362a1b [sensirion_common] Fix incorrect Big Endian conversion (#13492) 2026-01-23 16:19:41 -10:00
dependabot[bot]
e4763f8e71 Bump ruff from 0.14.13 to 0.14.14 (#13487)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-01-23 16:12:17 -10:00
Daniel Kent
9fddd0659e [bmp581] Split into bmp581_base and bmp581_i2c (#12485)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2026-01-23 19:28:14 -06:00
Keith Burzinski
faea546a0e [light] Fix cwww state restore (#13493) 2026-01-23 18:53:20 -06:00
Clyde Stubbs
069db2e128 [lvgl] Fix setting empty text (#13494) 2026-01-24 11:44:34 +11:00
Big Mike
5f2203b915 [sen5x] Fix store baseline functionality (#13469) 2026-01-23 18:03:23 -05:00
J. Nick Koston
5c67e04fef [slow_pwm] Fix dump_summary deprecation warning (#13460) 2026-01-23 12:37:06 -10:00
Clyde Stubbs
0cdcacc7fc [mipi_rgb] Add software reset command to st7701s init sequence (#13470) 2026-01-24 09:02:27 +11:00
139 changed files with 1696 additions and 1803 deletions

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
with:
category: "/language:${{matrix.language}}"

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.13
rev: v0.14.14
hooks:
# Run the linter.
- id: ruff

View File

@@ -88,7 +88,8 @@ esphome/components/bmp3xx/* @latonita
esphome/components/bmp3xx_base/* @latonita @martgras
esphome/components/bmp3xx_i2c/* @latonita
esphome/components/bmp3xx_spi/* @latonita
esphome/components/bmp581/* @kahrendt
esphome/components/bmp581_base/* @danielkent-net @kahrendt
esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid
esphome/components/bthome_mithermometer/* @nagyrobi
@@ -481,6 +482,7 @@ esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb
esphome/components/sx126x/* @swoboda1337
esphome/components/sx127x/* @swoboda1337
esphome/components/sy6970/* @linkedupbits
esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes
esphome/components/tc74/* @sethgirvan

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable
import json
import sys
from typing import TYPE_CHECKING
@@ -439,28 +438,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
return "\n".join(lines)
def to_json(self) -> str:
"""Export analysis results as JSON."""
data = {
"components": {
name: {
"text": mem.text_size,
"rodata": mem.rodata_size,
"data": mem.data_size,
"bss": mem.bss_size,
"flash_total": mem.flash_total,
"ram_total": mem.ram_total,
"symbol_count": mem.symbol_count,
}
for name, mem in self.components.items()
},
"totals": {
"flash": sum(c.flash_total for c in self.components.values()),
"ram": sum(c.ram_total for c in self.components.values()),
},
}
return json.dumps(data, indent=2)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
"""Dump uncategorized symbols for analysis."""
# Sort by size descending

View File

@@ -133,8 +133,8 @@ void APIConnection::start() {
return;
}
// Initialize client name with peername (IP address) until Hello message provides actual name
const char *peername = this->helper_->get_client_peername();
this->helper_->set_client_name(peername, strlen(peername));
char peername[socket::SOCKADDR_STR_LEN];
this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
}
APIConnection::~APIConnection() {
@@ -179,8 +179,8 @@ void APIConnection::begin_iterator_(ActiveIterator type) {
void APIConnection::loop() {
if (this->flags_.next_close) {
// requested a disconnect
this->helper_->close();
// requested a disconnect - don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
this->flags_.remove = true;
return;
}
@@ -293,7 +293,8 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
}
void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
this->helper_->close();
// Don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
this->flags_.remove = true;
}
@@ -1524,8 +1525,11 @@ void APIConnection::complete_authentication_() {
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_client_peername()));
{
char peername[socket::SOCKADDR_STR_LEN];
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_peername_to(peername)));
}
#endif
#ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1544,8 +1548,9 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor;
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp;
resp.api_version_major = 1;
@@ -1862,7 +1867,8 @@ void APIConnection::on_no_setup_connection() {
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
}
void APIConnection::on_fatal_error() {
this->helper_->close();
// Don't close socket here - keep it open so getpeername() works for logging
// Socket will be closed when client is removed from the list in APIServer::loop()
this->flags_.remove = true;
}
@@ -2218,12 +2224,14 @@ void APIConnection::process_state_subscriptions_() {
#endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_client_(int level, const LogString *message) {
char peername[socket::SOCKADDR_STR_LEN];
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
this->helper_->get_client_peername(), LOG_STR_ARG(message));
this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
}
void APIConnection::log_warning_(const LogString *message, APIError err) {
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(),
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_peername_to(peername),
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
}

View File

@@ -281,8 +281,8 @@ class APIConnection final : public APIServerConnection {
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const char *get_name() const { return this->helper_->get_client_name(); }
/// Get peer name (IP address) - cached at connection init time
const char *get_peername() const { return this->helper_->get_client_peername(); }
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
const char *get_peername_to(char *buf) const { return this->helper_->get_peername_to(buf); }
protected:
// Helper function to handle authentication completion

View File

@@ -16,7 +16,12 @@ static const char *const TAG = "api.frame_helper";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
@@ -240,13 +245,20 @@ APIError APIFrameHelper::try_send_tx_buf_() {
return APIError::OK; // All buffers sent successfully
}
const char *APIFrameHelper::get_peername_to(char *buf) const {
if (this->socket_) {
this->socket_->getpeername_to(std::span<char, socket::SOCKADDR_STR_LEN>(buf, socket::SOCKADDR_STR_LEN));
} else {
buf[0] = '\0';
}
return buf;
}
APIError APIFrameHelper::init_common_() {
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
// Cache peername now while socket is valid - needed for error logging after socket failure
this->socket_->getpeername_to(this->client_peername_);
int err = this->socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;

View File

@@ -90,8 +90,9 @@ class APIFrameHelper {
// Get client name (null-terminated)
const char *get_client_name() const { return this->client_name_; }
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
const char *get_client_peername() const { return this->client_peername_; }
// Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
// Returns pointer to buf for convenience in printf-style calls
const char *get_peername_to(char *buf) const;
// Set client name from buffer with length (truncates if needed)
void set_client_name(const char *name, size_t len) {
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
@@ -105,6 +106,8 @@ class APIFrameHelper {
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
if (state_ == State::CLOSED)
return APIError::OK; // Already closed
state_ = State::CLOSED;
int err = this->socket_->close();
if (err == -1)
@@ -231,8 +234,6 @@ class APIFrameHelper {
// Client name buffer - stores name from Hello message or initial peername
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
// Cached peername/IP address - captured at init time for availability after socket failure
char client_peername_[socket::SOCKADDR_STR_LEN]{};
// Group smaller types together
uint16_t rx_buf_len_ = 0;

View File

@@ -29,7 +29,12 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -21,7 +21,12 @@ static const char *const TAG = "api.plaintext";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -192,11 +192,15 @@ void APIServer::loop() {
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before removal for the trigger
// Save client info before closing socket and removal for the trigger
char peername_buf[socket::SOCKADDR_STR_LEN];
std::string client_name(client->get_name());
std::string client_peername(client->get_peername());
std::string client_peername(client->get_peername_to(peername_buf));
#endif
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());

View File

@@ -3,7 +3,6 @@
#include "bedjet_hub.h"
#include "bedjet_child.h"
#include "bedjet_const.h"
#include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/core/application.h"
#include <cinttypes>

View File

@@ -1,164 +1,5 @@
import math
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PASCAL,
CONFIG_SCHEMA = cv.invalid(
"The bmp581 sensor component has been renamed to bmp581_i2c."
)
CODEOWNERS = ["@kahrendt"]
DEPENDENCIES = ["i2c"]
bmp581_ns = cg.esphome_ns.namespace("bmp581")
Oversampling = bmp581_ns.enum("Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": Oversampling.OVERSAMPLING_NONE,
"2X": Oversampling.OVERSAMPLING_X2,
"4X": Oversampling.OVERSAMPLING_X4,
"8X": Oversampling.OVERSAMPLING_X8,
"16X": Oversampling.OVERSAMPLING_X16,
"32X": Oversampling.OVERSAMPLING_X32,
"64X": Oversampling.OVERSAMPLING_X64,
"128X": Oversampling.OVERSAMPLING_X128,
}
IIRFilter = bmp581_ns.enum("IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": IIRFilter.IIR_FILTER_OFF,
"2X": IIRFilter.IIR_FILTER_2,
"4X": IIRFilter.IIR_FILTER_4,
"8X": IIRFilter.IIR_FILTER_8,
"16X": IIRFilter.IIR_FILTER_16,
"32X": IIRFilter.IIR_FILTER_32,
"64X": IIRFilter.IIR_FILTER_64,
"128X": IIRFilter.IIR_FILTER_128,
}
BMP581Component = bmp581_ns.class_(
"BMP581Component", cg.PollingComponent, i2c.I2CDevice
)
def compute_measurement_conversion_time(config):
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
# - returns a rounded up time in ms
# Page 12 of datasheet
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.7,
"4X": 2.9,
"8X": 5.4,
"16X": 10.4,
"32X": 20.4,
"64X": 40.4,
"128X": 80.4,
}
# Page 12 of datasheet
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.1,
"4X": 1.5,
"8X": 2.1,
"16X": 3.3,
"32X": 5.8,
"64X": 10.8,
"128X": 20.8,
}
pressure_conversion_time = (
0.0 # No conversion time necessary without a pressure sensor
)
if pressure_config := config.get(CONF_PRESSURE):
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
pressure_config.get(CONF_OVERSAMPLING)
]
temperature_conversion_time = (
1.0 # BMP581 always samples the temperature even if only reading pressure
)
if temperature_config := config.get(CONF_TEMPERATURE):
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
temperature_config.get(CONF_OVERSAMPLING)
]
# Datasheet indicates a 5% possible error in each conversion time listed
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BMP581Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_PASCAL,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x46))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(
var.set_temperature_oversampling_config(
temperature_config[CONF_OVERSAMPLING]
)
)
cg.add(
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
)
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))

View File

@@ -0,0 +1,157 @@
import math
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PASCAL,
)
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
bmp581_ns = cg.esphome_ns.namespace("bmp581_base")
Oversampling = bmp581_ns.enum("Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": Oversampling.OVERSAMPLING_NONE,
"2X": Oversampling.OVERSAMPLING_X2,
"4X": Oversampling.OVERSAMPLING_X4,
"8X": Oversampling.OVERSAMPLING_X8,
"16X": Oversampling.OVERSAMPLING_X16,
"32X": Oversampling.OVERSAMPLING_X32,
"64X": Oversampling.OVERSAMPLING_X64,
"128X": Oversampling.OVERSAMPLING_X128,
}
IIRFilter = bmp581_ns.enum("IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": IIRFilter.IIR_FILTER_OFF,
"2X": IIRFilter.IIR_FILTER_2,
"4X": IIRFilter.IIR_FILTER_4,
"8X": IIRFilter.IIR_FILTER_8,
"16X": IIRFilter.IIR_FILTER_16,
"32X": IIRFilter.IIR_FILTER_32,
"64X": IIRFilter.IIR_FILTER_64,
"128X": IIRFilter.IIR_FILTER_128,
}
BMP581Component = bmp581_ns.class_("BMP581Component", cg.PollingComponent)
def compute_measurement_conversion_time(config):
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
# - returns a rounded up time in ms
# Page 12 of datasheet
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.7,
"4X": 2.9,
"8X": 5.4,
"16X": 10.4,
"32X": 20.4,
"64X": 40.4,
"128X": 80.4,
}
# Page 12 of datasheet
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.1,
"4X": 1.5,
"8X": 2.1,
"16X": 3.3,
"32X": 5.8,
"64X": 10.8,
"128X": 20.8,
}
pressure_conversion_time = (
0.0 # No conversion time necessary without a pressure sensor
)
if pressure_config := config.get(CONF_PRESSURE):
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
pressure_config.get(CONF_OVERSAMPLING)
]
temperature_conversion_time = (
1.0 # BMP581 always samples the temperature even if only reading pressure
)
if temperature_config := config.get(CONF_TEMPERATURE):
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
temperature_config.get(CONF_OVERSAMPLING)
]
# Datasheet indicates a 5% possible error in each conversion time listed
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
CONFIG_SCHEMA_BASE = cv.Schema(
{
cv.GenerateID(): cv.declare_id(BMP581Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_PASCAL,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
}
).extend(cv.polling_component_schema("60s"))
async def to_code_base(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(
var.set_temperature_oversampling_config(
temperature_config[CONF_OVERSAMPLING]
)
)
cg.add(
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
)
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))
return var

View File

@@ -10,12 +10,11 @@
* - All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4)
*/
#include "bmp581.h"
#include "bmp581_base.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace bmp581 {
namespace esphome::bmp581_base {
static const char *const TAG = "bmp581";
@@ -91,7 +90,6 @@ void BMP581Component::dump_config() {
break;
}
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_);
@@ -149,7 +147,7 @@ void BMP581Component::setup() {
uint8_t chip_id;
// read chip id from sensor
if (!this->read_byte(BMP581_CHIP_ID, &chip_id)) {
if (!this->bmp_read_byte(BMP581_CHIP_ID, &chip_id)) {
ESP_LOGE(TAG, "Read chip ID failed");
this->error_code_ = ERROR_COMMUNICATION_FAILED;
@@ -172,7 +170,7 @@ void BMP581Component::setup() {
// 3) Verify sensor status (check if NVM is okay) //
////////////////////////////////////////////////////
if (!this->read_byte(BMP581_STATUS, &this->status_.reg)) {
if (!this->bmp_read_byte(BMP581_STATUS, &this->status_.reg)) {
ESP_LOGE(TAG, "Failed to read status register");
this->error_code_ = ERROR_COMMUNICATION_FAILED;
@@ -359,7 +357,7 @@ bool BMP581Component::check_data_readiness_() {
uint8_t status;
if (!this->read_byte(BMP581_INT_STATUS, &status)) {
if (!this->bmp_read_byte(BMP581_INT_STATUS, &status)) {
ESP_LOGE(TAG, "Failed to read interrupt status register");
return false;
}
@@ -400,7 +398,7 @@ bool BMP581Component::prime_iir_filter_() {
// flush the IIR filter with forced measurements (we will only flush once)
this->dsp_config_.bit.iir_flush_forced_en = true;
if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
ESP_LOGE(TAG, "Failed to write IIR source register");
return false;
@@ -430,7 +428,7 @@ bool BMP581Component::prime_iir_filter_() {
// disable IIR filter flushings on future forced measurements
this->dsp_config_.bit.iir_flush_forced_en = false;
if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
ESP_LOGE(TAG, "Failed to write IIR source register");
return false;
@@ -454,7 +452,7 @@ bool BMP581Component::read_temperature_(float &temperature) {
}
uint8_t data[3];
if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
ESP_LOGW(TAG, "Failed to read measurement");
this->status_set_warning();
@@ -483,7 +481,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float &
}
uint8_t data[6];
if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
ESP_LOGW(TAG, "Failed to read measurement");
this->status_set_warning();
@@ -507,7 +505,7 @@ bool BMP581Component::reset_() {
// - returns the Power-On-Reboot interrupt status, which is asserted if successful
// writes reset command to BMP's command register
if (!this->write_byte(BMP581_COMMAND, RESET_COMMAND)) {
if (!this->bmp_write_byte(BMP581_COMMAND, RESET_COMMAND)) {
ESP_LOGE(TAG, "Failed to write reset command");
return false;
@@ -518,7 +516,7 @@ bool BMP581Component::reset_() {
delay(3);
// read interrupt status register
if (!this->read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
if (!this->bmp_read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
ESP_LOGE(TAG, "Failed to read interrupt status register");
return false;
@@ -562,7 +560,7 @@ bool BMP581Component::write_iir_settings_(IIRFilter temperature_iir, IIRFilter p
// BMP581_DSP register and BMP581_DSP_IIR registers are successive
// - allows us to write the IIR configuration with one command to both registers
uint8_t register_data[2] = {this->dsp_config_.reg, this->iir_config_.reg};
return this->write_bytes(BMP581_DSP, register_data, sizeof(register_data));
return this->bmp_write_bytes(BMP581_DSP, register_data, sizeof(register_data));
}
bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
@@ -572,7 +570,7 @@ bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
this->int_source_.bit.drdy_data_reg_en = data_ready_enable;
// write interrupt source register
return this->write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
return this->bmp_write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
}
bool BMP581Component::write_oversampling_settings_(Oversampling temperature_oversampling,
@@ -583,7 +581,7 @@ bool BMP581Component::write_oversampling_settings_(Oversampling temperature_over
this->osr_config_.bit.osr_t = temperature_oversampling;
this->osr_config_.bit.osr_p = pressure_oversampling;
return this->write_byte(BMP581_OSR, this->osr_config_.reg);
return this->bmp_write_byte(BMP581_OSR, this->osr_config_.reg);
}
bool BMP581Component::write_power_mode_(OperationMode mode) {
@@ -593,8 +591,7 @@ bool BMP581Component::write_power_mode_(OperationMode mode) {
this->odr_config_.bit.pwr_mode = mode;
// write odr register
return this->write_byte(BMP581_ODR, this->odr_config_.reg);
return this->bmp_write_byte(BMP581_ODR, this->odr_config_.reg);
}
} // namespace bmp581
} // namespace esphome
} // namespace esphome::bmp581_base

View File

@@ -3,11 +3,9 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace bmp581 {
namespace esphome::bmp581_base {
static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet)
static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command
@@ -59,7 +57,7 @@ enum IIRFilter {
IIR_FILTER_128 = 0x7
};
class BMP581Component : public PollingComponent, public i2c::I2CDevice {
class BMP581Component : public PollingComponent {
public:
void dump_config() override;
@@ -84,6 +82,11 @@ class BMP581Component : public PollingComponent, public i2c::I2CDevice {
void set_conversion_time(uint8_t conversion_time) { this->conversion_time_ = conversion_time; }
protected:
virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0;
virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0;
virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
virtual bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
@@ -216,5 +219,4 @@ class BMP581Component : public PollingComponent, public i2c::I2CDevice {
} odr_config_ = {.reg = 0};
};
} // namespace bmp581
} // namespace esphome
} // namespace esphome::bmp581_base

View File

@@ -0,0 +1,12 @@
#include "bmp581_i2c.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::bmp581_i2c {
void BMP581I2CComponent::dump_config() {
LOG_I2C_DEVICE(this);
BMP581Component::dump_config();
}
} // namespace esphome::bmp581_i2c

View File

@@ -0,0 +1,24 @@
#pragma once
#include "esphome/components/bmp581_base/bmp581_base.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::bmp581_i2c {
static const char *const TAG = "bmp581_i2c.sensor";
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
class BMP581I2CComponent : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
public:
bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); }
bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); }
bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
return read_bytes(a_register, data, len);
}
bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
return write_bytes(a_register, data, len);
}
void dump_config() override;
};
} // namespace esphome::bmp581_i2c

View File

@@ -0,0 +1,23 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base
AUTO_LOAD = ["bmp581_base"]
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
DEPENDENCIES = ["i2c"]
bmp581_ns = cg.esphome_ns.namespace("bmp581_i2c")
BMP581I2CComponent = bmp581_ns.class_(
"BMP581I2CComponent", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
i2c.i2c_device_schema(default_address=0x46)
).extend({cv.GenerateID(): cv.declare_id(BMP581I2CComponent)})
async def to_code(config):
var = await to_code_base(config)
await i2c.register_i2c_device(var, config)

View File

@@ -96,16 +96,10 @@ void CaptivePortal::start() {
}
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
#ifdef USE_ESP32
char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
StringRef url = req->url_to(url_buf);
#else
const auto &url = req->url();
#endif
if (url == ESPHOME_F("/config.json")) {
if (req->url() == ESPHOME_F("/config.json")) {
this->handle_config(req);
return;
} else if (url == ESPHOME_F("/wifisave")) {
} else if (req->url() == ESPHOME_F("/wifisave")) {
this->handle_wifisave(req);
return;
}

View File

@@ -63,13 +63,11 @@ def validate_auto_clear(value):
return cv.boolean(value)
def basic_display_schema(default_update_interval: str = "1s") -> cv.Schema:
"""Create a basic display schema with configurable default update interval."""
return cv.Schema(
{
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema(default_update_interval))
BASIC_DISPLAY_SCHEMA = cv.Schema(
{
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema("1s"))
def _validate_test_card(config):
@@ -83,41 +81,34 @@ def _validate_test_card(config):
return config
def full_display_schema(default_update_interval: str = "1s") -> cv.Schema:
"""Create a full display schema with configurable default update interval."""
schema = basic_display_schema(default_update_interval).extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(): cv.declare_id(DisplayPage),
cv.Required(CONF_LAMBDA): cv.lambda_,
}
),
cv.Length(min=1),
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
cv.GenerateID(): cv.declare_id(DisplayPage),
cv.Required(CONF_LAMBDA): cv.lambda_,
}
),
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
schema.add_extra(_validate_test_card)
return schema
BASIC_DISPLAY_SCHEMA = basic_display_schema("1s")
FULL_DISPLAY_SCHEMA = full_display_schema("1s")
cv.Length(min=1),
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
}
),
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
async def setup_display_core_(var, config):

View File

@@ -31,7 +31,6 @@ from esphome.const import (
CONF_TRANSFORM,
CONF_UPDATE_INTERVAL,
CONF_WIDTH,
SCHEDULER_DONT_RUN,
)
from esphome.cpp_generator import RawExpression
from esphome.final_validate import full_config
@@ -73,10 +72,12 @@ TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
minimum_update_interval = update_interval(
model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s")
)
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
return (
display.full_display_schema("60s")
.extend(
display.FULL_DISPLAY_SCHEMA.extend(
spi.spi_device_schema(
cs_pin_required=False,
default_mode="MODE0",
@@ -93,6 +94,9 @@ def model_schema(config):
{
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All(
update_interval, cv.Range(min=minimum_update_interval)
),
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
@@ -146,22 +150,15 @@ def _final_validate(config):
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
# If no drawing methods are configured, and LVGL is not enabled, show a test card
if (
CONF_LAMBDA not in config
and CONF_PAGES not in config
and LVGL_DOMAIN not in global_config
):
config[CONF_SHOW_TEST_CARD] = True
interval = config[CONF_UPDATE_INTERVAL]
if interval != SCHEDULER_DONT_RUN:
model = MODELS[config[CONF_MODEL]]
minimum = update_interval(model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s"))
if interval < minimum:
raise cv.Invalid(
f"update_interval must be at least {minimum} for {model.name}, got {interval}"
)
if CONF_LAMBDA not in config and CONF_PAGES not in config:
if LVGL_DOMAIN in global_config:
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
elif CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
return config

View File

@@ -676,9 +676,6 @@ CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
# Ring buffer IRAM requirement tracking
KEY_RINGBUF_IN_IRAM = "ringbuf_in_iram"
def require_vfs_select() -> None:
"""Mark that VFS select support is required by a component.
@@ -698,17 +695,6 @@ def require_vfs_dir() -> None:
CORE.data[KEY_VFS_DIR_REQUIRED] = True
def enable_ringbuf_in_iram() -> None:
"""Keep ring buffer functions in IRAM instead of moving them to flash.
Call this from components that use esphome/core/ring_buffer.cpp and need
the ring buffer functions to remain in IRAM for performance reasons
(e.g., voice assistants, audio components).
This prevents CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH from being enabled.
"""
CORE.data[KEY_RINGBUF_IN_IRAM] = True
def _parse_idf_component(value: str) -> ConfigType:
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
# Match operator followed by version-like string (digit or *)
@@ -1132,18 +1118,14 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True)
# Place ring buffer functions into flash instead of IRAM by default
# This saves IRAM but may impact performance for audio/voice components.
# Components that need ring buffer in IRAM call enable_ringbuf_in_iram().
# Users can also set ringbuf_in_iram: true to force IRAM placement.
# In ESP-IDF 6.0 flash placement becomes the default.
if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM] or CORE.data.get(
KEY_RINGBUF_IN_IRAM, False
):
# User config or component requires ring buffer in IRAM for performance
# This saves IRAM. In ESP-IDF 6.0 flash placement becomes the default.
# Users can set ringbuf_in_iram: true as an escape hatch if they encounter issues.
if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM]:
# User requests ring buffer in IRAM
# IDF 6.0+: will need CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=n
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH", False)
else:
# No component needs it - place in flash to save IRAM
# Place in flash to save IRAM (default)
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True)
# Place heap functions into flash to save IRAM (~4-6KB savings)

View File

@@ -85,6 +85,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
break;
}
gpio_set_intr_type(this->get_pin_num(), idf_type);
gpio_intr_enable(this->get_pin_num());
if (!isr_service_installed) {
auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
if (res != ESP_OK) {
@@ -94,7 +95,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
isr_service_installed = true;
}
gpio_isr_handler_add(this->get_pin_num(), func, arg);
gpio_intr_enable(this->get_pin_num());
}
size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {

View File

@@ -19,7 +19,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
std::unique_ptr<uint8_t[]> data;
size_t len;
void set_data(const uint8_t *src, size_t size) {
if (!this->data || this->len != size) {
this->data = std::make_unique<uint8_t[]>(size);
this->len = size;
}
memcpy(this->data.get(), src, size);
}
};
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -32,14 +41,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.data.set(data, len);
obj.set_data(data, len);
return true;
}
}
NVSData save{};
save.key = this->key;
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
save.set_data(data, len);
s_pending_save.emplace_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
@@ -47,11 +56,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.data.size() != len) {
if (obj.len != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
memcpy(data, obj.data.get(), len);
return true;
}
}
@@ -127,10 +136,10 @@ class ESP32Preferences : public ESPPreferences {
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
failed++;
last_err = err;
last_key = save.key;
@@ -138,7 +147,7 @@ class ESP32Preferences : public ESPPreferences {
}
written++;
} else {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
@@ -169,7 +178,7 @@ class ESP32Preferences : public ESPPreferences {
return true;
}
// Check size first before allocating memory
if (actual_len != to_save.data.size()) {
if (actual_len != to_save.len) {
return true;
}
// Most preferences are small, use stack buffer with heap fallback for large ones
@@ -179,7 +188,7 @@ class ESP32Preferences : public ESPPreferences {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return true;
}
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;
}
bool reset() override {

View File

@@ -98,10 +98,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
}
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
}
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
this->advertising_init_();
this->advertising_->set_manufacturer_data(data);
this->advertising_start();

View File

@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
void advertising_add_service_uuid(ESPBTUUID uuid);

View File

@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
}
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
this->set_manufacturer_data(std::span<const uint8_t>(data));
}
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
delete[] this->advertising_data_.p_manufacturer_data;
this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.manufacturer_len = data.size();

View File

@@ -37,7 +37,6 @@ class BLEAdvertising {
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_manufacturer_data(std::span<const uint8_t> data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_service_data(const std::vector<uint8_t> &data);
void set_service_data(std::span<const uint8_t> data);

View File

@@ -1,6 +1,5 @@
#include "esp32_ble_beacon.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32

View File

@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;

View File

@@ -12,6 +12,7 @@ from esphome.const import (
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE
from esphome.cpp_generator import add_define
CODEOWNERS = ["@swoboda1337"]
@@ -42,6 +43,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
add_define("USE_ESP32_HOSTED")
if config[CONF_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH",

View File

@@ -802,8 +802,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
#endif
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
/*
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
@@ -820,10 +820,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
#endif
}
}
#endif // USE_ETHERNET_KSZ8081

View File

@@ -71,7 +71,7 @@ void FanCall::validate_() {
auto traits = this->parent_.get_traits();
if (this->speed_.has_value()) {
this->speed_ = clamp(*this->speed_, 1, static_cast<int>(traits.supported_speed_count()));
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
// "Manually setting a speed must disable any set preset mode"

View File

@@ -11,7 +11,7 @@ namespace fan {
class FanTraits {
public:
FanTraits() = default;
FanTraits(bool oscillation, bool speed, bool direction, uint8_t speed_count)
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
/// Return if this fan supports oscillation.
@@ -23,9 +23,9 @@ class FanTraits {
/// Set whether this fan supports speed levels.
void set_speed(bool speed) { this->speed_ = speed; }
/// Return how many speed levels the fan has
uint8_t supported_speed_count() const { return this->speed_count_; }
int supported_speed_count() const { return this->speed_count_; }
/// Set how many speed levels this fan has.
void set_supported_speed_count(uint8_t speed_count) { this->speed_count_ = speed_count; }
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
/// Return if this fan supports changing direction
bool supports_direction() const { return this->direction_; }
/// Set whether this fan supports changing direction
@@ -64,7 +64,7 @@ class FanTraits {
bool oscillation_{false};
bool speed_{false};
bool direction_{false};
uint8_t speed_count_{};
int speed_count_{};
std::vector<const char *> preset_modes_{};
};

View File

@@ -9,55 +9,29 @@ from esphome.const import (
CONF_VALUE,
)
from esphome.core import CoroPriority, coroutine_with_priority
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
globals_ns = cg.esphome_ns.namespace("globals")
GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component)
RestoringGlobalsComponent = globals_ns.class_(
"RestoringGlobalsComponent", cg.PollingComponent
)
RestoringGlobalsComponent = globals_ns.class_("RestoringGlobalsComponent", cg.Component)
RestoringGlobalStringComponent = globals_ns.class_(
"RestoringGlobalStringComponent", cg.PollingComponent
"RestoringGlobalStringComponent", cg.Component
)
GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action)
CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length"
# Base schema fields shared by both variants
_BASE_SCHEMA = {
cv.Required(CONF_ID): cv.declare_id(GlobalsComponent),
cv.Required(CONF_TYPE): cv.string_strict,
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
}
# Non-restoring globals: regular Component (no polling needed)
_NON_RESTORING_SCHEMA = cv.Schema(
{
**_BASE_SCHEMA,
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
}
).extend(cv.COMPONENT_SCHEMA)
# Restoring globals: PollingComponent with configurable update_interval
_RESTORING_SCHEMA = cv.Schema(
{
**_BASE_SCHEMA,
cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean,
}
).extend(cv.polling_component_schema("1s"))
def _globals_schema(config: ConfigType) -> ConfigType:
"""Select schema based on restore_value setting."""
if config.get(CONF_RESTORE_VALUE, False):
return _RESTORING_SCHEMA(config)
return _NON_RESTORING_SCHEMA(config)
MULTI_CONF = True
CONFIG_SCHEMA = _globals_schema
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(GlobalsComponent),
cv.Required(CONF_TYPE): cv.string_strict,
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
}
).extend(cv.COMPONENT_SCHEMA)
# Run with low priority so that namespaces are registered first

View File

@@ -5,7 +5,8 @@
#include "esphome/core/helpers.h"
#include <cstring>
namespace esphome::globals {
namespace esphome {
namespace globals {
template<typename T> class GlobalsComponent : public Component {
public:
@@ -23,14 +24,13 @@ template<typename T> class GlobalsComponent : public Component {
T value_{};
};
template<typename T> class RestoringGlobalsComponent : public PollingComponent {
template<typename T> class RestoringGlobalsComponent : public Component {
public:
using value_type = T;
explicit RestoringGlobalsComponent() : PollingComponent(1000) {}
explicit RestoringGlobalsComponent(T initial_value) : PollingComponent(1000), value_(initial_value) {}
explicit RestoringGlobalsComponent() = default;
explicit RestoringGlobalsComponent(T initial_value) : value_(initial_value) {}
explicit RestoringGlobalsComponent(
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
: PollingComponent(1000) {
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
memcpy(this->value_, initial_value.data(), sizeof(T));
}
@@ -44,7 +44,7 @@ template<typename T> class RestoringGlobalsComponent : public PollingComponent {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void update() override { store_value_(); }
void loop() override { store_value_(); }
void on_shutdown() override { store_value_(); }
@@ -66,14 +66,13 @@ template<typename T> class RestoringGlobalsComponent : public PollingComponent {
};
// Use with string or subclasses of strings
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public PollingComponent {
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public Component {
public:
using value_type = T;
explicit RestoringGlobalStringComponent() : PollingComponent(1000) {}
explicit RestoringGlobalStringComponent(T initial_value) : PollingComponent(1000) { this->value_ = initial_value; }
explicit RestoringGlobalStringComponent() = default;
explicit RestoringGlobalStringComponent(T initial_value) { this->value_ = initial_value; }
explicit RestoringGlobalStringComponent(
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
: PollingComponent(1000) {
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
memcpy(this->value_, initial_value.data(), sizeof(T));
}
@@ -91,7 +90,7 @@ template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public P
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void update() override { store_value_(); }
void loop() override { store_value_(); }
void on_shutdown() override { store_value_(); }
@@ -145,4 +144,5 @@ template<typename T> T &id(GlobalsComponent<T> *value) { return value->value();
template<typename T> T &id(RestoringGlobalsComponent<T> *value) { return value->value(); }
template<typename T, uint8_t SZ> T &id(RestoringGlobalStringComponent<T, SZ> *value) { return value->value(); }
} // namespace esphome::globals
} // namespace globals
} // namespace esphome

View File

@@ -39,7 +39,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
DECAY_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}

View File

@@ -15,7 +15,7 @@ enum DecayMode {
class HBridgeFan : public Component, public fan::Fan {
public:
HBridgeFan(uint8_t speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
@@ -33,7 +33,7 @@ class HBridgeFan : public Component, public fan::Fan {
output::FloatOutput *pin_b_;
output::FloatOutput *enable_{nullptr};
output::BinaryOutput *oscillating_{nullptr};
uint8_t speed_count_{};
int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};

View File

@@ -119,7 +119,7 @@ void IDFI2CBus::dump_config() {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}
@@ -185,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
}
jobs[num_jobs++].command = I2C_MASTER_CMD_STOP;
ESP_LOGV(TAG, "Sending %zu jobs", num_jobs);
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 20);
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100);
if (err == ESP_ERR_INVALID_STATE) {
ESP_LOGV(TAG, "TX to %02X failed: not acked", address);
return ERROR_NOT_ACKNOWLEDGED;

View File

@@ -1,11 +1,6 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components.esp32 import (
add_idf_sdkconfig_option,
enable_ringbuf_in_iram,
get_esp32_variant,
)
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
@@ -15,6 +10,8 @@ from esphome.components.esp32.const import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
add_idf_sdkconfig_option,
get_esp32_variant,
)
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE
@@ -281,9 +278,6 @@ async def to_code(config):
# Helps avoid callbacks being skipped due to processor load
add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True)
# Keep ring buffer functions in IRAM for audio performance
enable_ringbuf_in_iram()
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
if CONF_I2S_BCLK_PIN in config:
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))

View File

@@ -267,26 +267,16 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
for (auto &scan : results) {
if (scan.get_is_hidden())
continue;
const char *ssid_cstr = scan.get_ssid().c_str();
// Check if we've already sent this SSID
bool duplicate = false;
for (const auto &seen : networks) {
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
duplicate = true;
break;
}
}
if (duplicate)
const std::string &ssid = scan.get_ssid();
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
continue;
// Only allocate std::string after confirming it's not a duplicate
std::string ssid(ssid_cstr);
// Send each ssid separately to avoid overflowing the buffer
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
std::vector<uint8_t> data =
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
this->send_response_(data);
networks.push_back(std::move(ssid));
networks.push_back(ssid);
}
// Send empty response to signify the end of the list.
std::vector<uint8_t> data =

View File

@@ -11,7 +11,7 @@ static const char *const TAG = "kuntze";
static const uint8_t CMD_READ_REG = 0x03;
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
// Maximum bytes to log for Modbus responses (2 registers = 4 bytes, plus byte count = 5 bytes)
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {

View File

@@ -18,7 +18,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
std::unique_ptr<uint8_t[]> data;
size_t len;
void set_data(const uint8_t *src, size_t size) {
if (!this->data || this->len != size) {
this->data = std::make_unique<uint8_t[]>(size);
this->len = size;
}
memcpy(this->data.get(), src, size);
}
};
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -33,14 +42,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.data.set(data, len);
obj.set_data(data, len);
return true;
}
}
NVSData save{};
save.key = this->key;
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
save.set_data(data, len);
s_pending_save.emplace_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
@@ -49,11 +58,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.data.size() != len) {
if (obj.len != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
memcpy(data, obj.data.get(), len);
return true;
}
}
@@ -117,11 +126,11 @@ class LibreTinyPreferences : public ESPPreferences {
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
if (this->is_changed_(&this->db, save, key_str)) {
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
fdb_blob_make(&this->blob, save.data.data(), save.data.size());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
fdb_blob_make(&this->blob, save.data.get(), save.len);
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
if (err != FDB_NO_ERR) {
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err);
failed++;
last_err = err;
last_key = save.key;
@@ -129,7 +138,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
written++;
} else {
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size());
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
@@ -153,7 +162,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
// Check size first - if different, data has changed
if (kv.value_len != to_save.data.size()) {
if (kv.value_len != to_save.len) {
return true;
}
@@ -167,7 +176,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
// Compare the actual data
return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0;
}
bool reset() override {

View File

@@ -391,7 +391,10 @@ void LightCall::transform_parameters_() {
min_mireds > 0.0f && max_mireds > 0.0f) {
ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
this->parent_->get_name().c_str());
if (this->has_color_temperature()) {
// Only compute cold_white/warm_white from color_temperature if they're not already explicitly set.
// This is important for state restoration, where both color_temperature and cold_white/warm_white
// are restored from flash - we want to preserve the saved cold_white/warm_white values.
if (this->has_color_temperature() && !this->has_cold_white() && !this->has_warm_white()) {
const float color_temp = clamp(this->color_temperature_, min_mireds, max_mireds);
const float range = max_mireds - min_mireds;
const float ww_fraction = (color_temp - min_mireds) / range;

View File

@@ -234,7 +234,6 @@ class Logger : public Component {
#endif
protected:
void write_msg_(const char *msg, size_t len);
// RAII guard for recursion flags - sets flag on construction, clears on destruction
class RecursionGuard {
public:
@@ -261,6 +260,7 @@ class Logger : public Component {
#endif
#endif
void process_messages_();
void write_msg_(const char *msg, size_t len);
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
// It's the caller's responsibility to initialize buffer_at (typically to 0)

View File

@@ -1,51 +0,0 @@
#ifdef USE_ESP8266
#include "logger.h"
#include "esphome/core/log.h"
namespace esphome::logger {
static const char *const TAG = "logger";
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
case UART_SELECTION_UART0:
case UART_SELECTION_UART0_SWAP:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
Serial.swap();
}
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
break;
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
break;
}
} else {
uart_set_debug(UART_NO);
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
case UART_SELECTION_UART0_SWAP:
default:
return LOG_STR("UART0_SWAP");
}
}
} // namespace esphome::logger
#endif

View File

@@ -1,22 +0,0 @@
#if defined(USE_HOST)
#include "logger.h"
namespace esphome::logger {
void HOT Logger::write_msg_(const char *msg) {
time_t rawtime;
struct tm *timeinfo;
char buffer[80];
time(&rawtime);
timeinfo = localtime(&rawtime);
strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo);
fputs(buffer, stdout);
puts(msg);
}
void Logger::pre_setup() { global_logger = this; }
} // namespace esphome::logger
#endif

View File

@@ -1,70 +0,0 @@
#ifdef USE_LIBRETINY
#include "logger.h"
namespace esphome::logger {
static const char *const TAG = "logger";
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
#if LT_HW_UART0
case UART_SELECTION_UART0:
this->hw_serial_ = &Serial0;
Serial0.begin(this->baud_rate_);
break;
#endif
#if LT_HW_UART1
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
break;
#endif
#if LT_HW_UART2
case UART_SELECTION_UART2:
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
break;
#endif
default:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
if (this->uart_ != UART_SELECTION_DEFAULT) {
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
"The default port was used instead.");
}
break;
}
// change lt_log() port to match default Serial
if (this->uart_ == UART_SELECTION_DEFAULT) {
this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1);
lt_log_set_port(LT_UART_DEFAULT_SERIAL);
} else {
lt_log_set_port(this->uart_ - 1);
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_DEFAULT:
return LOG_STR("DEFAULT");
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
case UART_SELECTION_UART2:
default:
return LOG_STR("UART2");
}
}
} // namespace esphome::logger
#endif // USE_LIBRETINY

View File

@@ -1,48 +0,0 @@
#ifdef USE_RP2040
#include "logger.h"
#include "esphome/core/log.h"
namespace esphome::logger {
static const char *const TAG = "logger";
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
case UART_SELECTION_UART0:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
break;
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
break;
case UART_SELECTION_USB_CDC:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
break;
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
return LOG_STR("USB_CDC");
#endif
default:
return LOG_STR("UNKNOWN");
}
}
} // namespace esphome::logger
#endif // USE_RP2040

View File

@@ -1,96 +0,0 @@
#ifdef USE_ZEPHYR
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "logger.h"
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/usb/usb_device.h>
namespace esphome::logger {
static const char *const TAG = "logger";
#ifdef USE_LOGGER_USB_CDC
void Logger::loop() {
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
return;
}
static bool opened = false;
uint32_t dtr = 0;
uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr);
/* Poll if the DTR flag was set, optional */
if (opened == dtr) {
return;
}
if (!opened) {
App.schedule_dump_config();
}
opened = !opened;
}
#endif
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
static const struct device *uart_dev = nullptr;
switch (this->uart_) {
case UART_SELECTION_UART0:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0));
break;
case UART_SELECTION_UART1:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
break;
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
if (device_is_ready(uart_dev)) {
usb_enable(nullptr);
}
break;
#endif
}
if (!device_is_ready(uart_dev)) {
ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_()));
} else {
this->uart_dev_ = uart_dev;
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) {
#ifdef CONFIG_PRINTK
printk("%s\n", msg);
#endif
if (nullptr == this->uart_dev_) {
return;
}
while (*msg) {
uart_poll_out(this->uart_dev_, *msg);
++msg;
}
uart_poll_out(this->uart_dev_, '\n');
}
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
return LOG_STR("USB_CDC");
#endif
default:
return LOG_STR("UNKNOWN");
}
}
} // namespace esphome::logger
#endif

View File

@@ -32,7 +32,7 @@ class LabelType(WidgetType):
async def to_code(self, w: Widget, config):
"""For a text object, create and set text"""
if value := config.get(CONF_TEXT):
if (value := config.get(CONF_TEXT)) is not None:
await w.set_property(CONF_TEXT, await lv_text.process(value))
await w.set_property(CONF_LONG_MODE, config)
await w.set_property(CONF_RECOLOR, config)

View File

@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
this->update_reg_(pin, false, iodir);
}
}
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
if (this->is_failed())
return false;

View File

@@ -448,9 +448,6 @@ async def to_code(config):
# The inference task queues detection events that need immediate processing
socket.require_wake_loop_threadsafe()
# Keep ring buffer functions in IRAM for audio performance
esp32.enable_ringbuf_in_iram()
mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE])
cg.add(var.set_microphone_source(mic_source))

View File

@@ -55,6 +55,7 @@ st7701s = ST7701S(
pclk_frequency="16MHz",
pclk_inverted=True,
initsequence=(
(0x01,), # Software Reset
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0
(0xC0, 0x3B, 0x00), (0xC1, 0x0D, 0x02), (0xC2, 0x31, 0x05),
(0xB0, 0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18,),

View File

@@ -279,7 +279,7 @@ def modbus_calc_properties(config):
if isinstance(value, str):
value = value.encode()
config[CONF_ADDRESS] = binascii.crc_hqx(value, 0)
config[CONF_REGISTER_TYPE] = ModbusRegisterType.CUSTOM
config[CONF_REGISTER_TYPE] = cv.enum(MODBUS_REGISTER_TYPE)("custom")
config[CONF_FORCE_NEW_RANGE] = True
return byte_offset, reg_count

View File

@@ -119,8 +119,7 @@ bool MQTTAlarmControlPanelComponent::publish_state() {
default:
state_s = "unknown";
}
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
return this->publish(this->get_state_topic_(), state_s);
}
} // namespace esphome::mqtt

View File

@@ -52,9 +52,8 @@ bool MQTTBinarySensorComponent::publish_state(bool state) {
if (this->binary_sensor_->is_status_binary_sensor())
return true;
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
const char *state_s = state ? "ON" : "OFF";
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
return this->publish(this->get_state_topic_(), state_s);
}
} // namespace esphome::mqtt

View File

@@ -132,29 +132,17 @@ std::string MQTTComponent::get_command_topic_() const {
}
bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
return this->publish(topic.c_str(), payload.data(), payload.size());
return this->publish(topic, payload.data(), payload.size());
}
bool MQTTComponent::publish(const std::string &topic, const char *payload, size_t payload_length) {
return this->publish(topic.c_str(), payload, payload_length);
}
bool MQTTComponent::publish(const char *topic, const char *payload, size_t payload_length) {
if (topic[0] == '\0')
if (topic.empty())
return false;
return global_mqtt_client->publish(topic, payload, payload_length, this->qos_, this->retain_);
}
bool MQTTComponent::publish(const char *topic, const char *payload) {
return this->publish(topic, payload, strlen(payload));
}
bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) {
return this->publish_json(topic.c_str(), f);
}
bool MQTTComponent::publish_json(const char *topic, const json::json_build_t &f) {
if (topic[0] == '\0')
if (topic.empty())
return false;
return global_mqtt_client->publish_json(topic, f, this->qos_, this->retain_);
}

View File

@@ -157,38 +157,6 @@ class MQTTComponent : public Component {
*/
bool publish(const std::string &topic, const char *payload, size_t payload_length);
/** Send a MQTT message (no heap allocation for topic).
*
* @param topic The topic as C string.
* @param payload The payload buffer.
* @param payload_length The length of the payload.
*/
bool publish(const char *topic, const char *payload, size_t payload_length);
/** Send a MQTT message (no heap allocation for topic).
*
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
* @param payload The payload buffer.
* @param payload_length The length of the payload.
*/
bool publish(StringRef topic, const char *payload, size_t payload_length) {
return this->publish(topic.c_str(), payload, payload_length);
}
/** Send a MQTT message (no heap allocation for topic).
*
* @param topic The topic as C string.
* @param payload The null-terminated payload.
*/
bool publish(const char *topic, const char *payload);
/** Send a MQTT message (no heap allocation for topic).
*
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
* @param payload The null-terminated payload.
*/
bool publish(StringRef topic, const char *payload) { return this->publish(topic.c_str(), payload); }
/** Construct and send a JSON MQTT message.
*
* @param topic The topic.
@@ -196,20 +164,6 @@ class MQTTComponent : public Component {
*/
bool publish_json(const std::string &topic, const json::json_build_t &f);
/** Construct and send a JSON MQTT message (no heap allocation for topic).
*
* @param topic The topic as C string.
* @param f The Json Message builder.
*/
bool publish_json(const char *topic, const json::json_build_t &f);
/** Construct and send a JSON MQTT message (no heap allocation for topic).
*
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
* @param f The Json Message builder.
*/
bool publish_json(StringRef topic, const json::json_build_t &f) { return this->publish_json(topic.c_str(), f); }
/** Subscribe to a MQTT topic.
*
* @param topic The topic. Wildcards are currently not supported.

View File

@@ -115,8 +115,7 @@ bool MQTTCoverComponent::publish_state() {
: this->cover_->position == COVER_OPEN ? "open"
: traits.get_supports_position() ? "open"
: "unknown";
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf), state_s))
if (!this->publish(this->get_state_topic_(), state_s))
success = false;
return success;
}

View File

@@ -53,8 +53,7 @@ bool MQTTDateComponent::send_initial_state() {
}
}
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [year, month, day](JsonObject root) {
return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("year")] = year;
root[ESPHOME_F("month")] = month;

View File

@@ -66,17 +66,15 @@ bool MQTTDateTimeComponent::send_initial_state() {
}
bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
uint8_t second) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf),
[year, month, day, hour, minute, second](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("year")] = year;
root[ESPHOME_F("month")] = month;
root[ESPHOME_F("day")] = day;
root[ESPHOME_F("hour")] = hour;
root[ESPHOME_F("minute")] = minute;
root[ESPHOME_F("second")] = second;
});
return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("year")] = year;
root[ESPHOME_F("month")] = month;
root[ESPHOME_F("day")] = day;
root[ESPHOME_F("hour")] = hour;
root[ESPHOME_F("minute")] = minute;
root[ESPHOME_F("second")] = second;
});
}
} // namespace esphome::mqtt

View File

@@ -44,8 +44,7 @@ void MQTTEventComponent::dump_config() {
}
bool MQTTEventComponent::publish_event_(const std::string &event_type) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [event_type](JsonObject root) {
return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[MQTT_EVENT_TYPE] = event_type;
});

View File

@@ -158,10 +158,9 @@ void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig
}
}
bool MQTTFanComponent::publish_state() {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
const char *state_s = this->state_->state ? "ON" : "OFF";
ESP_LOGD(TAG, "'%s' Sending state %s.", this->state_->get_name().c_str(), state_s);
this->publish(this->get_state_topic_to_(topic_buf), state_s);
this->publish(this->get_state_topic_(), state_s);
bool failed = false;
if (this->state_->get_traits().supports_direction()) {
bool success = this->publish(this->get_direction_state_topic(),

View File

@@ -34,8 +34,7 @@ void MQTTJSONLightComponent::on_light_remote_values_update() {
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
bool MQTTJSONLightComponent::publish_state_() {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [this](JsonObject root) {
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
LightJSONSchema::dump_json(*this->state_, root);
});

View File

@@ -47,14 +47,13 @@ void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfi
bool MQTTLockComponent::send_initial_state() { return this->publish_state(); }
bool MQTTLockComponent::publish_state() {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
#ifdef USE_STORE_LOG_STR_IN_FLASH
char buf[LOCK_STATE_STR_SIZE];
strncpy_P(buf, (PGM_P) lock_state_to_string(this->lock_->state), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
return this->publish(this->get_state_topic_to_(topic_buf), buf);
return this->publish(this->get_state_topic_(), buf);
#else
return this->publish(this->get_state_topic_to_(topic_buf), LOG_STR_ARG(lock_state_to_string(this->lock_->state)));
return this->publish(this->get_state_topic_(), LOG_STR_ARG(lock_state_to_string(this->lock_->state)));
#endif
}

View File

@@ -74,10 +74,9 @@ bool MQTTNumberComponent::send_initial_state() {
}
}
bool MQTTNumberComponent::publish_state(float value) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
char buffer[64];
size_t len = buf_append_printf(buffer, sizeof(buffer), 0, "%f", value);
return this->publish(this->get_state_topic_to_(topic_buf), buffer, len);
buf_append_printf(buffer, sizeof(buffer), 0, "%f", value);
return this->publish(this->get_state_topic_(), buffer);
}
} // namespace esphome::mqtt

View File

@@ -50,8 +50,7 @@ bool MQTTSelectComponent::send_initial_state() {
}
}
bool MQTTSelectComponent::publish_state(const std::string &value) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size());
return this->publish(this->get_state_topic_(), value);
}
} // namespace esphome::mqtt

View File

@@ -79,13 +79,12 @@ bool MQTTSensorComponent::send_initial_state() {
}
}
bool MQTTSensorComponent::publish_state(float value) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (mqtt::global_mqtt_client->is_publish_nan_as_none() && std::isnan(value))
return this->publish(this->get_state_topic_to_(topic_buf), "None", 4);
return this->publish(this->get_state_topic_(), "None", 4);
int8_t accuracy = this->sensor_->get_accuracy_decimals();
char buf[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(buf, value, accuracy);
return this->publish(this->get_state_topic_to_(topic_buf), buf, len);
return this->publish(this->get_state_topic_(), buf, len);
}
} // namespace esphome::mqtt

View File

@@ -52,9 +52,8 @@ void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); }
bool MQTTSwitchComponent::publish_state(bool state) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
const char *state_s = state ? "ON" : "OFF";
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
return this->publish(this->get_state_topic_(), state_s);
}
} // namespace esphome::mqtt

View File

@@ -53,8 +53,7 @@ bool MQTTTextComponent::send_initial_state() {
}
}
bool MQTTTextComponent::publish_state(const std::string &value) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size());
return this->publish(this->get_state_topic_(), value);
}
} // namespace esphome::mqtt

View File

@@ -31,10 +31,7 @@ void MQTTTextSensor::dump_config() {
LOG_MQTT_COMPONENT(true, false);
}
bool MQTTTextSensor::publish_state(const std::string &value) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size());
}
bool MQTTTextSensor::publish_state(const std::string &value) { return this->publish(this->get_state_topic_(), value); }
bool MQTTTextSensor::send_initial_state() {
if (this->sensor_->has_state()) {
return this->publish_state(this->sensor_->state);

View File

@@ -53,8 +53,7 @@ bool MQTTTimeComponent::send_initial_state() {
}
}
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [hour, minute, second](JsonObject root) {
return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("hour")] = hour;
root[ESPHOME_F("minute")] = minute;

View File

@@ -28,8 +28,7 @@ void MQTTUpdateComponent::setup() {
}
bool MQTTUpdateComponent::publish_state() {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [this](JsonObject root) {
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
root[ESPHOME_F("installed_version")] = this->update_->update_info.current_version;
root[ESPHOME_F("latest_version")] = this->update_->update_info.latest_version;
root[ESPHOME_F("title")] = this->update_->update_info.title;

View File

@@ -84,8 +84,7 @@ bool MQTTValveComponent::publish_state() {
: this->valve_->position == VALVE_OPEN ? "open"
: traits.get_supports_position() ? "open"
: "unknown";
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf), state_s))
if (!this->publish(this->get_state_topic_(), state_s))
success = false;
return success;
}

View File

@@ -41,14 +41,12 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); }
bool canHandle(AsyncWebServerRequest *request) const override {
if (request->method() != HTTP_GET)
return false;
#ifdef USE_ESP32
char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
return request->url_to(url_buf) == "/metrics";
#else
return request->url() == ESPHOME_F("/metrics");
#endif
if (request->method() == HTTP_GET) {
if (request->url() == "/metrics")
return true;
}
return false;
}
void handleRequest(AsyncWebServerRequest *req) override;

View File

@@ -133,14 +133,17 @@ void RD03DComponent::process_frame_() {
uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE);
// Extract raw bytes for this target
// Note: Despite datasheet Table 5-2 showing order as X, Y, Speed, Resolution,
// actual radar output has Resolution before Speed (verified empirically -
// stationary targets were showing non-zero speed with original field order)
uint8_t x_low = this->buffer_[offset + 0];
uint8_t x_high = this->buffer_[offset + 1];
uint8_t y_low = this->buffer_[offset + 2];
uint8_t y_high = this->buffer_[offset + 3];
uint8_t speed_low = this->buffer_[offset + 4];
uint8_t speed_high = this->buffer_[offset + 5];
uint8_t res_low = this->buffer_[offset + 6];
uint8_t res_high = this->buffer_[offset + 7];
uint8_t res_low = this->buffer_[offset + 4];
uint8_t res_high = this->buffer_[offset + 5];
uint8_t speed_low = this->buffer_[offset + 6];
uint8_t speed_high = this->buffer_[offset + 7];
// Decode values per RD-03D format
int16_t x = decode_value(x_low, x_high);

View File

@@ -80,21 +80,23 @@ class Select : public EntityBase {
void add_on_state_callback(std::function<void(size_t)> &&callback);
/** Set the value of the select by index, this is an optional virtual method.
*
* This method is called by the SelectCall when the index is already known.
* Default implementation converts to string and calls control().
* Override this to work directly with indices and avoid string conversions.
*
* @param index The index as validated by the SelectCall.
*/
virtual void control(size_t index) { this->control(this->option_at(index)); }
protected:
friend class SelectCall;
size_t active_index_{0};
/** Set the value of the select by index, this is an optional virtual method.
*
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
* Overriding this index-based version is PREFERRED as it avoids string conversions.
*
* This method is called by the SelectCall when the index is already known.
* Default implementation converts to string and calls control(const std::string&).
*
* @param index The index as validated by the SelectCall.
*/
virtual void control(size_t index) { this->control(this->option_at(index)); }
/** Set the value of the select, this is a virtual method that each select integration can implement.
*
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.

View File

@@ -30,6 +30,19 @@ static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10; //
static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor
static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor
static const LogString *type_to_string(Sen5xType type) {
switch (type) {
case Sen5xType::SEN50:
return LOG_STR("SEN50");
case Sen5xType::SEN54:
return LOG_STR("SEN54");
case Sen5xType::SEN55:
return LOG_STR("SEN55");
default:
return LOG_STR("UNKNOWN");
}
}
static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) {
switch (mode) {
case LOW_ACCELERATION:
@@ -43,6 +56,15 @@ static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) {
}
}
// This function performs an in-place conversion of the provided buffer
// from uint16_t values to big endianness
static inline const char *sensirion_convert_to_string_in_place(uint16_t *array, size_t length) {
for (size_t i = 0; i < length; i++) {
array[i] = convert_big_endian(array[i]);
}
return reinterpret_cast<const char *>(array);
}
void SEN5XComponent::setup() {
// the sensor needs 1000 ms to enter the idle state
this->set_timeout(1000, [this]() {
@@ -75,18 +97,18 @@ void SEN5XComponent::setup() {
stop_measurement_delay = 200;
}
this->set_timeout(stop_measurement_delay, [this]() {
uint16_t raw_serial_number[3];
if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) {
// note: serial number register is actually 32-bytes long but we grab only the first 16-bytes,
// this appears to be all that Sensirion uses for serial numbers, this could change
uint16_t raw_serial_number[8];
if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 8, 20)) {
ESP_LOGE(TAG, "Failed to read serial number");
this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
this->mark_failed();
return;
}
this->serial_number_[0] = static_cast<bool>(uint16_t(raw_serial_number[0]) & 0xFF);
this->serial_number_[1] = static_cast<uint16_t>(raw_serial_number[0] & 0xFF);
this->serial_number_[2] = static_cast<uint16_t>(raw_serial_number[1] >> 8);
ESP_LOGV(TAG, "Serial number %02d.%02d.%02d", this->serial_number_[0], this->serial_number_[1],
this->serial_number_[2]);
const char *serial_number = sensirion_convert_to_string_in_place(raw_serial_number, 8);
snprintf(this->serial_number_, sizeof(this->serial_number_), "%s", serial_number);
ESP_LOGV(TAG, "Serial number %s", this->serial_number_);
uint16_t raw_product_name[16];
if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
@@ -95,50 +117,35 @@ void SEN5XComponent::setup() {
this->mark_failed();
return;
}
// 2 ASCII bytes are encoded in an int
const uint16_t *current_int = raw_product_name;
char current_char;
uint8_t max = 16;
do {
// first char
current_char = *current_int >> 8;
if (current_char) {
this->product_name_.push_back(current_char);
// second char
current_char = *current_int & 0xFF;
if (current_char) {
this->product_name_.push_back(current_char);
}
}
current_int++;
} while (current_char && --max);
Sen5xType sen5x_type = UNKNOWN;
if (this->product_name_ == "SEN50") {
sen5x_type = SEN50;
const char *product_name = sensirion_convert_to_string_in_place(raw_product_name, 16);
if (strncmp(product_name, "SEN50", 5) == 0) {
this->type_ = Sen5xType::SEN50;
} else if (strncmp(product_name, "SEN54", 5) == 0) {
this->type_ = Sen5xType::SEN54;
} else if (strncmp(product_name, "SEN55", 5) == 0) {
this->type_ = Sen5xType::SEN55;
} else {
if (this->product_name_ == "SEN54") {
sen5x_type = SEN54;
} else {
if (this->product_name_ == "SEN55") {
sen5x_type = SEN55;
}
}
ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str());
this->type_ = Sen5xType::UNKNOWN;
ESP_LOGE(TAG, "Unknown product name: %.32s", product_name);
this->error_code_ = PRODUCT_NAME_FAILED;
this->mark_failed();
return;
}
if (this->humidity_sensor_ && sen5x_type == SEN50) {
ESP_LOGD(TAG, "Type: %s", LOG_STR_ARG(type_to_string(this->type_)));
if (this->humidity_sensor_ && this->type_ == Sen5xType::SEN50) {
ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55");
this->humidity_sensor_ = nullptr; // mark as not used
}
if (this->temperature_sensor_ && sen5x_type == SEN50) {
if (this->temperature_sensor_ && this->type_ == Sen5xType::SEN50) {
ESP_LOGE(TAG, "Temperature requires a SEN54 or SEN55");
this->temperature_sensor_ = nullptr; // mark as not used
}
if (this->voc_sensor_ && sen5x_type == SEN50) {
if (this->voc_sensor_ && this->type_ == Sen5xType::SEN50) {
ESP_LOGE(TAG, "VOC requires a SEN54 or SEN55");
this->voc_sensor_ = nullptr; // mark as not used
}
if (this->nox_sensor_ && sen5x_type != SEN55) {
if (this->nox_sensor_ && this->type_ != Sen5xType::SEN55) {
ESP_LOGE(TAG, "NOx requires a SEN55");
this->nox_sensor_ = nullptr; // mark as not used
}
@@ -153,43 +160,25 @@ void SEN5XComponent::setup() {
ESP_LOGV(TAG, "Firmware version %d", this->firmware_version_);
if (this->voc_sensor_ && this->store_baseline_) {
uint32_t combined_serial =
encode_uint24(this->serial_number_[0], this->serial_number_[1], this->serial_number_[2]);
// Hash with config hash, version, and serial number
// This ensures the baseline storage is cleared after OTA
// Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), combined_serial);
this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true);
if (this->pref_.load(&this->voc_baselines_storage_)) {
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
}
// Initialize storage timestamp
this->seconds_since_last_store_ = 0;
if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
uint16_t states[4];
states[0] = this->voc_baselines_storage_.state0 >> 16;
states[1] = this->voc_baselines_storage_.state0 & 0xFFFF;
states[2] = this->voc_baselines_storage_.state1 >> 16;
states[3] = this->voc_baselines_storage_.state1 & 0xFFFF;
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) {
ESP_LOGE(TAG, "Failed to set VOC baseline from saved state");
// Hash with serial number, serial numbers are unique, so multiple sensors can be used without conflict
uint32_t hash = fnv1a_hash(this->serial_number_);
this->pref_ = global_preferences->make_preference<uint16_t[4]>(hash, true);
this->voc_baseline_time_ = App.get_loop_component_start_time();
if (this->pref_.load(&this->voc_baseline_state_)) {
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, this->voc_baseline_state_, 4)) {
ESP_LOGE(TAG, "VOC Baseline State write to sensor failed");
} else {
ESP_LOGV(TAG, "VOC Baseline State loaded");
delay(20);
}
}
}
bool result;
if (this->auto_cleaning_interval_.has_value()) {
// override default value
result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value());
result = this->write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value());
} else {
result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
result = this->write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
}
if (result) {
delay(20);
@@ -276,11 +265,10 @@ void SEN5XComponent::dump_config() {
}
}
ESP_LOGCONFIG(TAG,
" Product name: %s\n"
" Type: %s\n"
" Firmware version: %d\n"
" Serial number %02d.%02d.%02d",
this->product_name_.c_str(), this->firmware_version_, this->serial_number_[0], this->serial_number_[1],
this->serial_number_[2]);
" Serial number: %s",
LOG_STR_ARG(type_to_string(this->type_)), this->firmware_version_, this->serial_number_);
if (this->auto_cleaning_interval_.has_value()) {
ESP_LOGCONFIG(TAG, " Auto cleaning interval: %" PRId32 "s", this->auto_cleaning_interval_.value());
}
@@ -288,6 +276,14 @@ void SEN5XComponent::dump_config() {
ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s",
LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value())));
}
if (this->voc_sensor_) {
char hex_buf[5 * 4];
format_hex_pretty_to(hex_buf, this->voc_baseline_state_, 4, 0);
ESP_LOGCONFIG(TAG,
" Store Baseline: %s\n"
" State: %s\n",
TRUEFALSE(this->store_baseline_), hex_buf);
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
@@ -304,36 +300,6 @@ void SEN5XComponent::update() {
return;
}
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
// much
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
// run it a bit later to avoid adding a delay here
this->set_timeout(550, [this]() {
uint16_t states[4];
if (this->read_data(states, 4)) {
uint32_t state0 = states[0] << 16 | states[1];
uint32_t state1 = states[2] << 16 | states[3];
if ((uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state0 - state0)) >
MAXIMUM_STORAGE_DIFF ||
(uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state1 - state1)) >
MAXIMUM_STORAGE_DIFF) {
this->seconds_since_last_store_ = 0;
this->voc_baselines_storage_.state0 = state0;
this->voc_baselines_storage_.state1 = state1;
if (this->pref_.save(&this->voc_baselines_storage_)) {
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
} else {
ESP_LOGW(TAG, "Could not store VOC baselines");
}
}
}
});
}
}
if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) {
this->status_set_warning();
ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_);
@@ -402,7 +368,29 @@ void SEN5XComponent::update() {
if (this->nox_sensor_ != nullptr) {
this->nox_sensor_->publish_state(nox);
}
this->status_clear_warning();
if (!this->voc_sensor_ || !this->store_baseline_ ||
(App.get_loop_component_start_time() - this->voc_baseline_time_) < SHORTEST_BASELINE_STORE_INTERVAL) {
this->status_clear_warning();
} else {
this->voc_baseline_time_ = App.get_loop_component_start_time();
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
this->status_set_warning();
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
} else {
this->set_timeout(20, [this]() {
if (!this->read_data(this->voc_baseline_state_, 4)) {
this->status_set_warning();
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
} else {
if (this->pref_.save(&this->voc_baseline_state_)) {
ESP_LOGD(TAG, "VOC Baseline State saved");
}
this->status_clear_warning();
}
});
}
}
});
}

View File

@@ -24,10 +24,7 @@ enum RhtAccelerationMode : uint16_t {
HIGH_ACCELERATION = 2,
};
struct Sen5xBaselines {
int32_t state0;
int32_t state1;
} PACKED; // NOLINT
enum class Sen5xType : uint8_t { SEN50, SEN54, SEN55, UNKNOWN };
struct GasTuning {
uint16_t index_offset;
@@ -44,11 +41,9 @@ struct TemperatureCompensation {
uint16_t time_constant;
};
// Shortest time interval of 3H for storing baseline values.
// Shortest time interval of 2H (in milliseconds) for storing baseline values.
// Prevents wear of the flash because of too many write operations
static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
// Store anyway if the baseline difference exceeds the max storage diff value
static const uint32_t MAXIMUM_STORAGE_DIFF = 50;
static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 2 * 60 * 60 * 1000;
class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
public:
@@ -56,20 +51,20 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
void dump_config() override;
void update() override;
enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN };
void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { this->pm_1_0_sensor_ = pm_1_0; }
void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { this->pm_2_5_sensor_ = pm_2_5; }
void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { this->pm_4_0_sensor_ = pm_4_0; }
void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { this->pm_10_0_sensor_ = pm_10_0; }
void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; }
void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; }
void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; }
void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; }
void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; }
void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
void set_acceleration_mode(RhtAccelerationMode mode) { acceleration_mode_ = mode; }
void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { auto_cleaning_interval_ = auto_cleaning_interval; }
void set_voc_sensor(sensor::Sensor *voc_sensor) { this->voc_sensor_ = voc_sensor; }
void set_nox_sensor(sensor::Sensor *nox_sensor) { this->nox_sensor_ = nox_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_store_baseline(bool store_baseline) { this->store_baseline_ = store_baseline; }
void set_acceleration_mode(RhtAccelerationMode mode) { this->acceleration_mode_ = mode; }
void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) {
this->auto_cleaning_interval_ = auto_cleaning_interval;
}
void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t std_initial, uint16_t gain_factor) {
@@ -80,7 +75,7 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
tuning_params.gating_max_duration_minutes = gating_max_duration_minutes;
tuning_params.std_initial = std_initial;
tuning_params.gain_factor = gain_factor;
voc_tuning_params_ = tuning_params;
this->voc_tuning_params_ = tuning_params;
}
void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
@@ -92,14 +87,14 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
tuning_params.gating_max_duration_minutes = gating_max_duration_minutes;
tuning_params.std_initial = 50;
tuning_params.gain_factor = gain_factor;
nox_tuning_params_ = tuning_params;
this->nox_tuning_params_ = tuning_params;
}
void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) {
TemperatureCompensation temp_comp;
temp_comp.offset = offset * 200;
temp_comp.normalized_offset_slope = normalized_offset_slope * 10000;
temp_comp.time_constant = time_constant;
temperature_compensation_ = temp_comp;
this->temperature_compensation_ = temp_comp;
}
bool start_fan_cleaning();
@@ -107,10 +102,12 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
bool write_temperature_compensation_(const TemperatureCompensation &compensation);
uint32_t seconds_since_last_store_;
char serial_number_[17] = "UNKNOWN";
uint16_t voc_baseline_state_[4]{0};
uint32_t voc_baseline_time_;
uint16_t firmware_version_;
Sen5xType type_{Sen5xType::UNKNOWN};
ERRORCODE error_code_;
uint8_t serial_number_[4];
bool initialized_{false};
bool store_baseline_;
@@ -131,8 +128,6 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
optional<GasTuning> nox_tuning_params_;
optional<TemperatureCompensation> temperature_compensation_;
ESPPreferenceObject pref_;
std::string product_name_;
Sen5xBaselines voc_baselines_storage_;
};
} // namespace sen5x

View File

@@ -210,6 +210,7 @@ SENSOR_MAP = {
SETTING_MAP = {
CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval",
CONF_ACCELERATION_MODE: "set_acceleration_mode",
CONF_STORE_BASELINE: "set_store_baseline",
}

View File

@@ -39,42 +39,23 @@ bool SensirionI2CDevice::read_data(uint16_t *data, const uint8_t len) {
*/
bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len, const uint16_t *data,
const uint8_t data_len) {
uint8_t temp_stack[BUFFER_STACK_SIZE];
std::unique_ptr<uint8_t[]> temp_heap;
uint8_t *temp;
size_t required_buffer_len = data_len * 3 + 2;
// Is a dynamic allocation required ?
if (required_buffer_len >= BUFFER_STACK_SIZE) {
temp_heap = std::unique_ptr<uint8_t[]>(new uint8_t[required_buffer_len]);
temp = temp_heap.get();
} else {
temp = temp_stack;
}
SmallBufferWithHeapFallback<BUFFER_STACK_SIZE> buffer(required_buffer_len);
uint8_t *temp = buffer.get();
// First byte or word is the command
uint8_t raw_idx = 0;
if (command_len == 1) {
temp[raw_idx++] = command & 0xFF;
} else {
// command is 2 bytes
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
temp[raw_idx++] = command >> 8;
temp[raw_idx++] = command & 0xFF;
#else
temp[raw_idx++] = command & 0xFF;
temp[raw_idx++] = command >> 8;
#endif
}
// add parameters followed by crc
// skipped if len == 0
for (size_t i = 0; i < data_len; i++) {
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
temp[raw_idx++] = data[i] >> 8;
temp[raw_idx++] = data[i] & 0xFF;
#else
temp[raw_idx++] = data[i] & 0xFF;
temp[raw_idx++] = data[i] >> 8;
#endif
// Use MSB first since Sensirion devices use CRC-8 with MSB first
uint8_t crc = crc8(&temp[raw_idx - 2], 2, 0xFF, CRC_POLYNOMIAL, true);
temp[raw_idx++] = crc;

View File

@@ -1,6 +1,7 @@
#include "slow_pwm_output.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
namespace esphome {
namespace slow_pwm {
@@ -20,7 +21,9 @@ void SlowPWMOutput::set_output_state_(bool new_state) {
}
if (new_state != current_state_) {
if (this->pin_) {
ESP_LOGV(TAG, "Switching output pin %s to %s", this->pin_->dump_summary().c_str(), ONOFF(new_state));
char pin_summary[GPIO_SUMMARY_MAX_LEN];
this->pin_->dump_summary(pin_summary, sizeof(pin_summary));
ESP_LOGV(TAG, "Switching output pin %s to %s", pin_summary, ONOFF(new_state));
} else {
ESP_LOGV(TAG, "Switching to %s", ONOFF(new_state));
}

View File

@@ -25,7 +25,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_SPEED): cv.invalid(
"Configuring individual speeds is deprecated."
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
)

View File

@@ -10,7 +10,7 @@ namespace speed {
class SpeedFan : public Component, public fan::Fan {
public:
SpeedFan(uint8_t speed_count) : speed_count_(speed_count) {}
SpeedFan(int speed_count) : speed_count_(speed_count) {}
void setup() override;
void dump_config() override;
void set_output(output::FloatOutput *output) { this->output_ = output; }
@@ -26,7 +26,7 @@ class SpeedFan : public Component, public fan::Fan {
output::FloatOutput *output_;
output::BinaryOutput *oscillating_{nullptr};
output::BinaryOutput *direction_{nullptr};
uint8_t speed_count_{};
int speed_count_{};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};
};

View File

@@ -0,0 +1,63 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID
CODEOWNERS = ["@linkedupbits"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
CONF_SY6970_ID = "sy6970_id"
CONF_ENABLE_STATUS_LED = "enable_status_led"
CONF_INPUT_CURRENT_LIMIT = "input_current_limit"
CONF_CHARGE_VOLTAGE = "charge_voltage"
CONF_CHARGE_CURRENT = "charge_current"
CONF_PRECHARGE_CURRENT = "precharge_current"
CONF_CHARGE_ENABLED = "charge_enabled"
CONF_ENABLE_ADC = "enable_adc"
sy6970_ns = cg.esphome_ns.namespace("sy6970")
SY6970Component = sy6970_ns.class_(
"SY6970Component", cg.PollingComponent, i2c.I2CDevice
)
SY6970Listener = sy6970_ns.class_("SY6970Listener")
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SY6970Component),
cv.Optional(CONF_ENABLE_STATUS_LED, default=True): cv.boolean,
cv.Optional(CONF_INPUT_CURRENT_LIMIT, default=500): cv.int_range(
min=100, max=3200
),
cv.Optional(CONF_CHARGE_VOLTAGE, default=4208): cv.int_range(
min=3840, max=4608
),
cv.Optional(CONF_CHARGE_CURRENT, default=2048): cv.int_range(
min=0, max=5056
),
cv.Optional(CONF_PRECHARGE_CURRENT, default=128): cv.int_range(
min=64, max=1024
),
cv.Optional(CONF_CHARGE_ENABLED, default=True): cv.boolean,
cv.Optional(CONF_ENABLE_ADC, default=True): cv.boolean,
}
)
.extend(cv.polling_component_schema("5s"))
.extend(i2c.i2c_device_schema(0x6A))
)
async def to_code(config):
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_ENABLE_STATUS_LED],
config[CONF_INPUT_CURRENT_LIMIT],
config[CONF_CHARGE_VOLTAGE],
config[CONF_CHARGE_CURRENT],
config[CONF_PRECHARGE_CURRENT],
config[CONF_CHARGE_ENABLED],
config[CONF_ENABLE_ADC],
)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

View File

@@ -0,0 +1,56 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_POWER
from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns
DEPENDENCIES = ["sy6970"]
CONF_VBUS_CONNECTED = "vbus_connected"
CONF_CHARGING = "charging"
CONF_CHARGE_DONE = "charge_done"
SY6970VbusConnectedBinarySensor = sy6970_ns.class_(
"SY6970VbusConnectedBinarySensor", binary_sensor.BinarySensor
)
SY6970ChargingBinarySensor = sy6970_ns.class_(
"SY6970ChargingBinarySensor", binary_sensor.BinarySensor
)
SY6970ChargeDoneBinarySensor = sy6970_ns.class_(
"SY6970ChargeDoneBinarySensor", binary_sensor.BinarySensor
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_SY6970_ID): cv.use_id(SY6970Component),
cv.Optional(CONF_VBUS_CONNECTED): binary_sensor.binary_sensor_schema(
SY6970VbusConnectedBinarySensor,
device_class=DEVICE_CLASS_CONNECTIVITY,
),
cv.Optional(CONF_CHARGING): binary_sensor.binary_sensor_schema(
SY6970ChargingBinarySensor,
device_class=DEVICE_CLASS_POWER,
),
cv.Optional(CONF_CHARGE_DONE): binary_sensor.binary_sensor_schema(
SY6970ChargeDoneBinarySensor,
device_class=DEVICE_CLASS_POWER,
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_SY6970_ID])
if vbus_connected_config := config.get(CONF_VBUS_CONNECTED):
sens = await binary_sensor.new_binary_sensor(vbus_connected_config)
cg.add(parent.add_listener(sens))
if charging_config := config.get(CONF_CHARGING):
sens = await binary_sensor.new_binary_sensor(charging_config)
cg.add(parent.add_listener(sens))
if charge_done_config := config.get(CONF_CHARGE_DONE):
sens = await binary_sensor.new_binary_sensor(charge_done_config)
cg.add(parent.add_listener(sens))

View File

@@ -0,0 +1,43 @@
#pragma once
#include "../sy6970.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome::sy6970 {
template<uint8_t REG, uint8_t SHIFT, uint8_t MASK, uint8_t TRUE_VALUE>
class StatusBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t value = (data.registers[REG] >> SHIFT) & MASK;
this->publish_state(value == TRUE_VALUE);
}
};
template<uint8_t REG, uint8_t SHIFT, uint8_t MASK, uint8_t FALSE_VALUE>
class InverseStatusBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t value = (data.registers[REG] >> SHIFT) & MASK;
this->publish_state(value != FALSE_VALUE);
}
};
// Custom binary sensor for charging (true when pre-charge or fast charge)
class SY6970ChargingBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t chrg_stat = (data.registers[SY6970_REG_STATUS] >> 3) & 0x03;
bool charging = chrg_stat != CHARGE_STATUS_NOT_CHARGING && chrg_stat != CHARGE_STATUS_CHARGE_DONE;
this->publish_state(charging);
}
};
// Specialized sensor types using templates
// VBUS connected: BUS_STATUS != NO_INPUT
using SY6970VbusConnectedBinarySensor = InverseStatusBinarySensor<SY6970_REG_STATUS, 5, 0x07, BUS_STATUS_NO_INPUT>;
// Charge done: CHARGE_STATUS == CHARGE_DONE
using SY6970ChargeDoneBinarySensor = StatusBinarySensor<SY6970_REG_STATUS, 3, 0x03, CHARGE_STATUS_CHARGE_DONE>;
} // namespace esphome::sy6970

View File

@@ -0,0 +1,95 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_BATTERY_VOLTAGE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
UNIT_MILLIAMP,
UNIT_VOLT,
)
from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns
DEPENDENCIES = ["sy6970"]
CONF_VBUS_VOLTAGE = "vbus_voltage"
CONF_SYSTEM_VOLTAGE = "system_voltage"
CONF_CHARGE_CURRENT = "charge_current"
CONF_PRECHARGE_CURRENT = "precharge_current"
SY6970VbusVoltageSensor = sy6970_ns.class_("SY6970VbusVoltageSensor", sensor.Sensor)
SY6970BatteryVoltageSensor = sy6970_ns.class_(
"SY6970BatteryVoltageSensor", sensor.Sensor
)
SY6970SystemVoltageSensor = sy6970_ns.class_("SY6970SystemVoltageSensor", sensor.Sensor)
SY6970ChargeCurrentSensor = sy6970_ns.class_("SY6970ChargeCurrentSensor", sensor.Sensor)
SY6970PrechargeCurrentSensor = sy6970_ns.class_(
"SY6970PrechargeCurrentSensor", sensor.Sensor
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_SY6970_ID): cv.use_id(SY6970Component),
cv.Optional(CONF_VBUS_VOLTAGE): sensor.sensor_schema(
SY6970VbusVoltageSensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
SY6970BatteryVoltageSensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_SYSTEM_VOLTAGE): sensor.sensor_schema(
SY6970SystemVoltageSensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CHARGE_CURRENT): sensor.sensor_schema(
SY6970ChargeCurrentSensor,
unit_of_measurement=UNIT_MILLIAMP,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PRECHARGE_CURRENT): sensor.sensor_schema(
SY6970PrechargeCurrentSensor,
unit_of_measurement=UNIT_MILLIAMP,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_SY6970_ID])
if vbus_voltage_config := config.get(CONF_VBUS_VOLTAGE):
sens = await sensor.new_sensor(vbus_voltage_config)
cg.add(parent.add_listener(sens))
if battery_voltage_config := config.get(CONF_BATTERY_VOLTAGE):
sens = await sensor.new_sensor(battery_voltage_config)
cg.add(parent.add_listener(sens))
if system_voltage_config := config.get(CONF_SYSTEM_VOLTAGE):
sens = await sensor.new_sensor(system_voltage_config)
cg.add(parent.add_listener(sens))
if charge_current_config := config.get(CONF_CHARGE_CURRENT):
sens = await sensor.new_sensor(charge_current_config)
cg.add(parent.add_listener(sens))
if precharge_current_config := config.get(CONF_PRECHARGE_CURRENT):
sens = await sensor.new_sensor(precharge_current_config)
cg.add(parent.add_listener(sens))

View File

@@ -0,0 +1,46 @@
#pragma once
#include "../sy6970.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::sy6970 {
// Template for voltage sensors (converts mV to V)
template<uint8_t REG, uint8_t MASK, uint16_t BASE, uint16_t STEP>
class VoltageSensor : public SY6970Listener, public sensor::Sensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t val = data.registers[REG] & MASK;
uint16_t voltage_mv = BASE + (val * STEP);
this->publish_state(voltage_mv * 0.001f); // Convert mV to V
}
};
// Template for current sensors (returns mA)
template<uint8_t REG, uint8_t MASK, uint16_t BASE, uint16_t STEP>
class CurrentSensor : public SY6970Listener, public sensor::Sensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t val = data.registers[REG] & MASK;
uint16_t current_ma = BASE + (val * STEP);
this->publish_state(current_ma);
}
};
// Specialized sensor types using templates
using SY6970VbusVoltageSensor = VoltageSensor<SY6970_REG_VBUS_VOLTAGE, 0x7F, VBUS_BASE_MV, VBUS_STEP_MV>;
using SY6970BatteryVoltageSensor = VoltageSensor<SY6970_REG_BATV, 0x7F, VBAT_BASE_MV, VBAT_STEP_MV>;
using SY6970SystemVoltageSensor = VoltageSensor<SY6970_REG_VINDPM_STATUS, 0x7F, VSYS_BASE_MV, VSYS_STEP_MV>;
using SY6970ChargeCurrentSensor = CurrentSensor<SY6970_REG_CHARGE_CURRENT_MONITOR, 0x7F, 0, CHG_CURRENT_STEP_MA>;
// Precharge current sensor needs special handling (bit shift)
class SY6970PrechargeCurrentSensor : public SY6970Listener, public sensor::Sensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t iprechg = (data.registers[SY6970_REG_PRECHARGE_CURRENT] >> 4) & 0x0F;
uint16_t iprechg_ma = PRE_CHG_BASE_MA + (iprechg * PRE_CHG_STEP_MA);
this->publish_state(iprechg_ma);
}
};
} // namespace esphome::sy6970

View File

@@ -0,0 +1,201 @@
#include "sy6970.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::sy6970 {
static const char *const TAG = "sy6970";
bool SY6970Component::read_all_registers_() {
// Read all registers from 0x00 to 0x14 in one transaction (21 bytes)
// This includes unused registers 0x0F, 0x10 for performance
if (!this->read_bytes(SY6970_REG_INPUT_CURRENT_LIMIT, this->data_.registers, 21)) {
ESP_LOGW(TAG, "Failed to read registers 0x00-0x14");
return false;
}
return true;
}
bool SY6970Component::write_register_(uint8_t reg, uint8_t value) {
if (!this->write_byte(reg, value)) {
ESP_LOGW(TAG, "Failed to write register 0x%02X", reg);
return false;
}
return true;
}
bool SY6970Component::update_register_(uint8_t reg, uint8_t mask, uint8_t value) {
uint8_t reg_value;
if (!this->read_byte(reg, &reg_value)) {
ESP_LOGW(TAG, "Failed to read register 0x%02X for update", reg);
return false;
}
reg_value = (reg_value & ~mask) | (value & mask);
return this->write_register_(reg, reg_value);
}
void SY6970Component::setup() {
ESP_LOGV(TAG, "Setting up SY6970...");
// Try to read chip ID
uint8_t reg_value;
if (!this->read_byte(SY6970_REG_DEVICE_ID, &reg_value)) {
ESP_LOGE(TAG, "Failed to communicate with SY6970");
this->mark_failed();
return;
}
uint8_t chip_id = reg_value & 0x03;
if (chip_id != 0x00) {
ESP_LOGW(TAG, "Unexpected chip ID: 0x%02X (expected 0x00)", chip_id);
}
// Apply configuration options (all have defaults now)
ESP_LOGV(TAG, "Setting LED enabled to %s", ONOFF(this->led_enabled_));
this->set_led_enabled(this->led_enabled_);
ESP_LOGV(TAG, "Setting input current limit to %u mA", this->input_current_limit_);
this->set_input_current_limit(this->input_current_limit_);
ESP_LOGV(TAG, "Setting charge voltage to %u mV", this->charge_voltage_);
this->set_charge_target_voltage(this->charge_voltage_);
ESP_LOGV(TAG, "Setting charge current to %u mA", this->charge_current_);
this->set_charge_current(this->charge_current_);
ESP_LOGV(TAG, "Setting precharge current to %u mA", this->precharge_current_);
this->set_precharge_current(this->precharge_current_);
ESP_LOGV(TAG, "Setting charge enabled to %s", ONOFF(this->charge_enabled_));
this->set_charge_enabled(this->charge_enabled_);
ESP_LOGV(TAG, "Setting ADC measurements to %s", ONOFF(this->enable_adc_));
this->set_enable_adc_measure(this->enable_adc_);
ESP_LOGV(TAG, "SY6970 initialized successfully");
}
void SY6970Component::dump_config() {
ESP_LOGCONFIG(TAG,
"SY6970:\n"
" LED Enabled: %s\n"
" Input Current Limit: %u mA\n"
" Charge Voltage: %u mV\n"
" Charge Current: %u mA\n"
" Precharge Current: %u mA\n"
" Charge Enabled: %s\n"
" ADC Enabled: %s",
ONOFF(this->led_enabled_), this->input_current_limit_, this->charge_voltage_, this->charge_current_,
this->precharge_current_, ONOFF(this->charge_enabled_), ONOFF(this->enable_adc_));
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with SY6970 failed!");
}
}
void SY6970Component::update() {
if (this->is_failed()) {
return;
}
// Read all registers in one transaction
if (!this->read_all_registers_()) {
ESP_LOGW(TAG, "Failed to read registers during update");
this->status_set_warning();
return;
}
this->status_clear_warning();
// Notify all listeners with the new data
for (auto *listener : this->listeners_) {
listener->on_data(this->data_);
}
}
void SY6970Component::set_input_current_limit(uint16_t milliamps) {
if (this->is_failed())
return;
if (milliamps < INPUT_CURRENT_MIN) {
milliamps = INPUT_CURRENT_MIN;
}
uint8_t val = (milliamps - INPUT_CURRENT_MIN) / INPUT_CURRENT_STEP;
if (val > 0x3F) {
val = 0x3F;
}
this->update_register_(SY6970_REG_INPUT_CURRENT_LIMIT, 0x3F, val);
}
void SY6970Component::set_charge_target_voltage(uint16_t millivolts) {
if (this->is_failed())
return;
if (millivolts < CHG_VOLTAGE_BASE) {
millivolts = CHG_VOLTAGE_BASE;
}
uint8_t val = (millivolts - CHG_VOLTAGE_BASE) / CHG_VOLTAGE_STEP;
if (val > 0x3F) {
val = 0x3F;
}
this->update_register_(SY6970_REG_CHARGE_VOLTAGE, 0xFC, val << 2);
}
void SY6970Component::set_precharge_current(uint16_t milliamps) {
if (this->is_failed())
return;
if (milliamps < PRE_CHG_BASE_MA) {
milliamps = PRE_CHG_BASE_MA;
}
uint8_t val = (milliamps - PRE_CHG_BASE_MA) / PRE_CHG_STEP_MA;
if (val > 0x0F) {
val = 0x0F;
}
this->update_register_(SY6970_REG_PRECHARGE_CURRENT, 0xF0, val << 4);
}
void SY6970Component::set_charge_current(uint16_t milliamps) {
if (this->is_failed())
return;
uint8_t val = milliamps / 64;
if (val > 0x7F) {
val = 0x7F;
}
this->update_register_(SY6970_REG_CHARGE_CURRENT, 0x7F, val);
}
void SY6970Component::set_charge_enabled(bool enabled) {
if (this->is_failed())
return;
this->update_register_(SY6970_REG_SYS_CONTROL, 0x10, enabled ? 0x10 : 0x00);
}
void SY6970Component::set_led_enabled(bool enabled) {
if (this->is_failed())
return;
// Bit 6: 0 = LED enabled, 1 = LED disabled
this->update_register_(SY6970_REG_TIMER_CONTROL, 0x40, enabled ? 0x00 : 0x40);
}
void SY6970Component::set_enable_adc_measure(bool enabled) {
if (this->is_failed())
return;
// Set bits to enable ADC conversion
this->update_register_(SY6970_REG_ADC_CONTROL, 0xC0, enabled ? 0xC0 : 0x00);
}
} // namespace esphome::sy6970

View File

@@ -0,0 +1,122 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include <vector>
namespace esphome::sy6970 {
// SY6970 Register addresses with descriptive names
static const uint8_t SY6970_REG_INPUT_CURRENT_LIMIT = 0x00; // Input current limit control
static const uint8_t SY6970_REG_VINDPM = 0x01; // Input voltage limit
static const uint8_t SY6970_REG_ADC_CONTROL = 0x02; // ADC control and function disable
static const uint8_t SY6970_REG_SYS_CONTROL = 0x03; // Charge enable and system config
static const uint8_t SY6970_REG_CHARGE_CURRENT = 0x04; // Fast charge current limit
static const uint8_t SY6970_REG_PRECHARGE_CURRENT = 0x05; // Pre-charge/termination current
static const uint8_t SY6970_REG_CHARGE_VOLTAGE = 0x06; // Charge voltage limit
static const uint8_t SY6970_REG_TIMER_CONTROL = 0x07; // Charge timer and status LED control
static const uint8_t SY6970_REG_IR_COMP = 0x08; // IR compensation
static const uint8_t SY6970_REG_FORCE_DPDM = 0x09; // Force DPDM detection
static const uint8_t SY6970_REG_BOOST_CONTROL = 0x0A; // Boost mode voltage/current
static const uint8_t SY6970_REG_STATUS = 0x0B; // System status (bus, charge status)
static const uint8_t SY6970_REG_FAULT = 0x0C; // Fault status (NTC)
static const uint8_t SY6970_REG_VINDPM_STATUS = 0x0D; // Input voltage limit status (also sys voltage)
static const uint8_t SY6970_REG_BATV = 0x0E; // Battery voltage
static const uint8_t SY6970_REG_VBUS_VOLTAGE = 0x11; // VBUS voltage
static const uint8_t SY6970_REG_CHARGE_CURRENT_MONITOR = 0x12; // Charge current
static const uint8_t SY6970_REG_INPUT_VOLTAGE_LIMIT = 0x13; // Input voltage limit
static const uint8_t SY6970_REG_DEVICE_ID = 0x14; // Part information
// Constants for voltage and current calculations
static const uint16_t VBUS_BASE_MV = 2600; // mV
static const uint16_t VBUS_STEP_MV = 100; // mV
static const uint16_t VBAT_BASE_MV = 2304; // mV
static const uint16_t VBAT_STEP_MV = 20; // mV
static const uint16_t VSYS_BASE_MV = 2304; // mV
static const uint16_t VSYS_STEP_MV = 20; // mV
static const uint16_t CHG_CURRENT_STEP_MA = 50; // mA
static const uint16_t PRE_CHG_BASE_MA = 64; // mA
static const uint16_t PRE_CHG_STEP_MA = 64; // mA
static const uint16_t CHG_VOLTAGE_BASE = 3840; // mV
static const uint16_t CHG_VOLTAGE_STEP = 16; // mV
static const uint16_t INPUT_CURRENT_MIN = 100; // mA
static const uint16_t INPUT_CURRENT_STEP = 50; // mA
// Bus Status values (REG_0B[7:5])
enum BusStatus {
BUS_STATUS_NO_INPUT = 0,
BUS_STATUS_USB_SDP = 1,
BUS_STATUS_USB_CDP = 2,
BUS_STATUS_USB_DCP = 3,
BUS_STATUS_HVDCP = 4,
BUS_STATUS_ADAPTER = 5,
BUS_STATUS_NO_STD_ADAPTER = 6,
BUS_STATUS_OTG = 7,
};
// Charge Status values (REG_0B[4:3])
enum ChargeStatus {
CHARGE_STATUS_NOT_CHARGING = 0,
CHARGE_STATUS_PRE_CHARGE = 1,
CHARGE_STATUS_FAST_CHARGE = 2,
CHARGE_STATUS_CHARGE_DONE = 3,
};
// Structure to hold all register data read in one transaction
struct SY6970Data {
uint8_t registers[21]; // Registers 0x00-0x14 (includes unused 0x0F, 0x10)
};
// Listener interface for components that want to receive SY6970 data updates
class SY6970Listener {
public:
virtual void on_data(const SY6970Data &data) = 0;
};
class SY6970Component : public PollingComponent, public i2c::I2CDevice {
public:
SY6970Component(bool led_enabled, uint16_t input_current_limit, uint16_t charge_voltage, uint16_t charge_current,
uint16_t precharge_current, bool charge_enabled, bool enable_adc)
: led_enabled_(led_enabled),
input_current_limit_(input_current_limit),
charge_voltage_(charge_voltage),
charge_current_(charge_current),
precharge_current_(precharge_current),
charge_enabled_(charge_enabled),
enable_adc_(enable_adc) {}
void setup() override;
void dump_config() override;
void update() override;
float get_setup_priority() const override { return setup_priority::DATA; }
// Listener registration
void add_listener(SY6970Listener *listener) { this->listeners_.push_back(listener); }
// Configuration methods to be called from lambdas
void set_input_current_limit(uint16_t milliamps);
void set_charge_target_voltage(uint16_t millivolts);
void set_precharge_current(uint16_t milliamps);
void set_charge_current(uint16_t milliamps);
void set_charge_enabled(bool enabled);
void set_led_enabled(bool enabled);
void set_enable_adc_measure(bool enabled = true);
protected:
bool read_all_registers_();
bool write_register_(uint8_t reg, uint8_t value);
bool update_register_(uint8_t reg, uint8_t mask, uint8_t value);
SY6970Data data_{};
std::vector<SY6970Listener *> listeners_;
// Configuration values to set during setup()
bool led_enabled_;
uint16_t input_current_limit_;
uint16_t charge_voltage_;
uint16_t charge_current_;
uint16_t precharge_current_;
bool charge_enabled_;
bool enable_adc_;
};
} // namespace esphome::sy6970

View File

@@ -0,0 +1,52 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from .. import CONF_SY6970_ID, SY6970Component, sy6970_ns
DEPENDENCIES = ["sy6970"]
CONF_BUS_STATUS = "bus_status"
CONF_CHARGE_STATUS = "charge_status"
CONF_NTC_STATUS = "ntc_status"
SY6970BusStatusTextSensor = sy6970_ns.class_(
"SY6970BusStatusTextSensor", text_sensor.TextSensor
)
SY6970ChargeStatusTextSensor = sy6970_ns.class_(
"SY6970ChargeStatusTextSensor", text_sensor.TextSensor
)
SY6970NtcStatusTextSensor = sy6970_ns.class_(
"SY6970NtcStatusTextSensor", text_sensor.TextSensor
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_SY6970_ID): cv.use_id(SY6970Component),
cv.Optional(CONF_BUS_STATUS): text_sensor.text_sensor_schema(
SY6970BusStatusTextSensor
),
cv.Optional(CONF_CHARGE_STATUS): text_sensor.text_sensor_schema(
SY6970ChargeStatusTextSensor
),
cv.Optional(CONF_NTC_STATUS): text_sensor.text_sensor_schema(
SY6970NtcStatusTextSensor
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_SY6970_ID])
if bus_status_config := config.get(CONF_BUS_STATUS):
sens = await text_sensor.new_text_sensor(bus_status_config)
cg.add(parent.add_listener(sens))
if charge_status_config := config.get(CONF_CHARGE_STATUS):
sens = await text_sensor.new_text_sensor(charge_status_config)
cg.add(parent.add_listener(sens))
if ntc_status_config := config.get(CONF_NTC_STATUS):
sens = await text_sensor.new_text_sensor(ntc_status_config)
cg.add(parent.add_listener(sens))

View File

@@ -0,0 +1,96 @@
#pragma once
#include "../sy6970.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome::sy6970 {
// Bus status text sensor
class SY6970BusStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t status = (data.registers[SY6970_REG_STATUS] >> 5) & 0x07;
const char *status_str = this->get_bus_status_string_(status);
this->publish_state(status_str);
}
protected:
const char *get_bus_status_string_(uint8_t status) {
switch (status) {
case BUS_STATUS_NO_INPUT:
return "No Input";
case BUS_STATUS_USB_SDP:
return "USB SDP";
case BUS_STATUS_USB_CDP:
return "USB CDP";
case BUS_STATUS_USB_DCP:
return "USB DCP";
case BUS_STATUS_HVDCP:
return "HVDCP";
case BUS_STATUS_ADAPTER:
return "Adapter";
case BUS_STATUS_NO_STD_ADAPTER:
return "Non-Standard Adapter";
case BUS_STATUS_OTG:
return "OTG";
default:
return "Unknown";
}
}
};
// Charge status text sensor
class SY6970ChargeStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t status = (data.registers[SY6970_REG_STATUS] >> 3) & 0x03;
const char *status_str = this->get_charge_status_string_(status);
this->publish_state(status_str);
}
protected:
const char *get_charge_status_string_(uint8_t status) {
switch (status) {
case CHARGE_STATUS_NOT_CHARGING:
return "Not Charging";
case CHARGE_STATUS_PRE_CHARGE:
return "Pre-charge";
case CHARGE_STATUS_FAST_CHARGE:
return "Fast Charge";
case CHARGE_STATUS_CHARGE_DONE:
return "Charge Done";
default:
return "Unknown";
}
}
};
// NTC status text sensor
class SY6970NtcStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t status = data.registers[SY6970_REG_FAULT] & 0x07;
const char *status_str = this->get_ntc_status_string_(status);
this->publish_state(status_str);
}
protected:
const char *get_ntc_status_string_(uint8_t status) {
switch (status) {
case 0:
return "Normal";
case 2:
return "Warm";
case 3:
return "Cool";
case 5:
return "Cold";
case 6:
return "Hot";
default:
return "Unknown";
}
}
};
} // namespace esphome::sy6970

View File

@@ -19,7 +19,7 @@ CONFIG_SCHEMA = (
{
cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean,
cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean,
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
)

View File

@@ -12,7 +12,7 @@ class TemplateFan final : public Component, public fan::Fan {
void dump_config() override;
void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; }
void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; }
void set_speed_count(uint8_t count) { this->speed_count_ = count; }
void set_speed_count(int count) { this->speed_count_ = count; }
void set_preset_modes(std::initializer_list<const char *> presets) { this->preset_modes_ = presets; }
fan::FanTraits get_traits() override { return this->traits_; }
@@ -21,7 +21,7 @@ class TemplateFan final : public Component, public fan::Fan {
bool has_oscillating_{false};
bool has_direction_{false};
uint8_t speed_count_{0};
int speed_count_{0};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};
};

View File

@@ -20,7 +20,7 @@ from .. import template_ns
CONF_CURRENT_TEMPERATURE = "current_temperature"
TemplateWaterHeater = template_ns.class_(
"TemplateWaterHeater", water_heater.WaterHeater
"TemplateWaterHeater", cg.Component, water_heater.WaterHeater
)
TemplateWaterHeaterPublishAction = template_ns.class_(
@@ -36,24 +36,29 @@ RESTORE_MODES = {
"RESTORE_AND_CALL": TemplateWaterHeaterRestoreMode.WATER_HEATER_RESTORE_AND_CALL,
}
CONFIG_SCHEMA = water_heater.water_heater_schema(TemplateWaterHeater).extend(
{
cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean,
cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum(
RESTORE_MODES, upper=True
),
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda,
cv.Optional(CONF_MODE): cv.returning_lambda,
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
water_heater.validate_water_heater_mode
),
}
CONFIG_SCHEMA = (
water_heater.water_heater_schema(TemplateWaterHeater)
.extend(
{
cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean,
cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum(
RESTORE_MODES, upper=True
),
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda,
cv.Optional(CONF_MODE): cv.returning_lambda,
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
water_heater.validate_water_heater_mode
),
}
)
.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 water_heater.register_water_heater(var, config)
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))

View File

@@ -10,7 +10,7 @@ TemplateWaterHeater::TemplateWaterHeater() : set_trigger_(new Trigger<>()) {}
void TemplateWaterHeater::setup() {
if (this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE ||
this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE_AND_CALL) {
auto restore = this->restore_state();
auto restore = this->restore_state_();
if (restore.has_value()) {
restore->perform();

Some files were not shown because too many files have changed in this diff Show More