Compare commits

..

56 Commits

Author SHA1 Message Date
J. Nick Koston
b445d46888 Merge remote-tracking branch 'upstream/dev' into mqtt_enum_flash
# Conflicts:
#	esphome/components/mqtt/mqtt_alarm_control_panel.cpp
#	esphome/components/mqtt/mqtt_component.cpp
#	esphome/components/mqtt/mqtt_component.h
#	esphome/components/mqtt/mqtt_cover.cpp
#	esphome/components/mqtt/mqtt_valve.cpp
2026-01-26 17:30:37 -10:00
J. Nick Koston
bf92d94863 [mqtt] Use stack buffers for publish_state() topic building (#13434)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-26 17:25:02 -10:00
J. Nick Koston
9c3817f544 [sml] Use constexpr std::array for START_SEQ constant (#13506) 2026-01-26 17:21:17 -10:00
J. Nick Koston
ee9e3315b6 [tm1638] Use member array instead of heap allocation for display buffer (#13504) 2026-01-26 17:21:05 -10:00
J. Nick Koston
67dea1e538 [light] Use member array instead of heap allocation in AddressableLightWrapper (#13503) 2026-01-26 17:20:49 -10:00
J. Nick Koston
003b9c6c3f [uln2003] Refactor step mode logging to use LogString (#13543) 2026-01-26 17:20:33 -10:00
J. Nick Koston
2f1a345905 [mhz19] Refactor detection range logging to use LogString (#13541) 2026-01-26 17:20:21 -10:00
J. Nick Koston
7ef933abec [libretiny] Bump to 1.11.0 (#13512) 2026-01-26 17:20:08 -10:00
J. Nick Koston
4ddd40bcfb [core] Add PROGMEM string comparison helpers and use in cover/valve/helpers (#13545) 2026-01-26 17:19:50 -10:00
J. Nick Koston
8ae901b3f1 [http_request] Use stack allocation for MD5 buffer in OTA (#13550) 2026-01-26 17:19:30 -10:00
J. Nick Koston
bc49174920 Add additional text_sensor filter tests (#13479) 2026-01-26 17:18:36 -10:00
J. Nick Koston
123ee02d39 [ota] Improve error message when device closes connection without responding (#13562) 2026-01-26 17:13:18 -10:00
Jonathan Swoboda
0cc8055757 [http_request] Add custom CA certificate support for ESP32 (#13552)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 22:07:27 -05:00
dependabot[bot]
27a212c14d Bump aioesphomeapi from 43.13.0 to 43.14.0 (#13557)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 15:43:40 -10:00
dependabot[bot]
65dc182526 Bump setuptools from 80.10.1 to 80.10.2 (#13558)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 15:43:27 -10:00
dependabot[bot]
dd91039ff1 Bump github/codeql-action from 4.31.11 to 4.32.0 (#13559)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 15:43:16 -10:00
J. Nick Koston
6da4f95258 [mqtt] Refactor state publishing with dedicated enum-to-string helpers 2026-01-25 23:04:33 -10:00
J. Nick Koston
0207e6e8b5 [mqtt] Refactor state publishing with dedicated enum-to-string helpers 2026-01-25 23:04:26 -10:00
J. Nick Koston
ee6e12913c [mqtt] Refactor state publishing with dedicated enum-to-string helpers 2026-01-25 23:02:18 -10:00
J. Nick Koston
d95ef154aa Merge branch 'dev' into mqtt_stack_part_2 2026-01-25 22:37:19 -10:00
sebcaps
1c9a9c7536 [mhz19] Fix Uninitialized var warning message (#13526) 2026-01-25 20:07:29 -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
J. Nick Koston
e006216ad3 Merge branch 'dev' into mqtt_stack_part_2 2026-01-21 18:37:55 -10:00
J. Nick Koston
d66d05dbfc [mqtt] Use stack buffers for publish_state() topic building 2026-01-21 11:08:06 -10:00
J. Nick Koston
bba447e656 Merge branch 'dev' into mqtt_formatting 2026-01-21 11:06:26 -10:00
J. Nick Koston
77b6720a25 Merge branch 'dev' into mqtt_formatting 2026-01-19 17:40:37 -10:00
J. Nick Koston
2f8f052f43 Merge branch 'dev' into mqtt_formatting 2026-01-18 18:44:11 -10:00
J. Nick Koston
86e70c7e76 more 2026-01-17 07:38:51 -10:00
J. Nick Koston
40025bb277 tweaks to reduce RAM 2026-01-17 07:34:22 -10:00
J. Nick Koston
438bb96687 tweaks to reduce RAM 2026-01-17 07:28:44 -10:00
J. Nick Koston
c1e1325af2 Merge branch 'dev' into mqtt_formatting 2026-01-17 07:24:11 -10:00
pre-commit-ci-lite[bot]
944194e04e [pre-commit.ci lite] apply automatic fixes 2026-01-15 00:02:35 +00:00
J. Nick Koston
d27d6d64da Update esphome/components/mqtt/mqtt_component.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-14 14:00:57 -10:00
J. Nick Koston
2182d1e9f0 [mqtt] Use stack buffers for discovery message formatting 2026-01-14 13:57:45 -10:00
91 changed files with 1797 additions and 530 deletions

View File

@@ -1 +1 @@
15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce
d565b0589e35e692b5f2fc0c14723a99595b4828a3a3ef96c442e86a23176c00

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
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@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
with:
category: "/language:${{matrix.language}}"

View File

@@ -482,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

@@ -1,11 +1,11 @@
#include "cover.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <strings.h>
#include "esphome/core/log.h"
namespace esphome::cover {
static const char *const TAG = "cover";
@@ -39,13 +39,13 @@ Cover::Cover() : position{COVER_OPEN} {}
CoverCall::CoverCall(Cover *parent) : parent_(parent) {}
CoverCall &CoverCall::set_command(const char *command) {
if (strcasecmp(command, "OPEN") == 0) {
if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("OPEN")) == 0) {
this->set_command_open();
} else if (strcasecmp(command, "CLOSE") == 0) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLOSE")) == 0) {
this->set_command_close();
} else if (strcasecmp(command, "STOP") == 0) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) {
this->set_command_stop();
} else if (strcasecmp(command, "TOGGLE") == 0) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) {
this->set_command_toggle();
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command);

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

@@ -126,7 +126,7 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_CA_CERTIFICATE_PATH): cv.All(
cv.file_,
cv.only_on(PLATFORM_HOST),
cv.Any(cv.only_on(PLATFORM_HOST), cv.only_on_esp32),
),
}
).extend(cv.COMPONENT_SCHEMA),
@@ -160,7 +160,14 @@ async def to_code(config):
cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL]))
if config.get(CONF_VERIFY_SSL):
esp32.add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
if ca_cert_path := config.get(CONF_CA_CERTIFICATE_PATH):
with open(ca_cert_path, encoding="utf-8") as f:
ca_cert_content = f.read()
cg.add(var.set_ca_certificate(ca_cert_content))
else:
esp32.add_idf_sdkconfig_option(
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_INSECURE",

View File

@@ -27,8 +27,9 @@ void HttpRequestIDF::dump_config() {
HttpRequestComponent::dump_config();
ESP_LOGCONFIG(TAG,
" Buffer Size RX: %u\n"
" Buffer Size TX: %u",
this->buffer_size_rx_, this->buffer_size_tx_);
" Buffer Size TX: %u\n"
" Custom CA Certificate: %s",
this->buffer_size_rx_, this->buffer_size_tx_, YESNO(this->ca_certificate_ != nullptr));
}
esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
@@ -88,11 +89,15 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
config.disable_auto_redirect = !this->follow_redirects_;
config.max_redirection_count = this->redirect_limit_;
config.auth_type = HTTP_AUTH_TYPE_BASIC;
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
if (secure && this->verify_ssl_) {
config.crt_bundle_attach = esp_crt_bundle_attach;
}
if (this->ca_certificate_ != nullptr) {
config.cert_pem = this->ca_certificate_;
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
} else {
config.crt_bundle_attach = esp_crt_bundle_attach;
#endif
}
}
if (this->useragent_ != nullptr) {
config.user_agent = this->useragent_;

View File

@@ -35,6 +35,7 @@ class HttpRequestIDF : public HttpRequestComponent {
void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; }
void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; }
void set_verify_ssl(bool verify_ssl) { this->verify_ssl_ = verify_ssl; }
void set_ca_certificate(const char *ca_certificate) { this->ca_certificate_ = ca_certificate; }
protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
@@ -44,6 +45,7 @@ class HttpRequestIDF : public HttpRequestComponent {
uint16_t buffer_size_rx_{};
uint16_t buffer_size_tx_{};
bool verify_ssl_{true};
const char *ca_certificate_{nullptr};
/// @brief Monitors the http client events to gather response headers
static esp_err_t http_event_handler(esp_http_client_event_t *evt);

View File

@@ -82,7 +82,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
uint32_t last_progress = 0;
uint32_t update_start_time = millis();
md5::MD5Digest md5_receive;
std::unique_ptr<char[]> md5_receive_str(new char[33]);
char md5_receive_str[33];
if (this->md5_expected_.empty() && !this->http_get_md5_()) {
return OTA_MD5_INVALID;
@@ -176,14 +176,14 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
// verify MD5 is as expected and act accordingly
md5_receive.calculate();
md5_receive.get_hex(md5_receive_str.get());
this->md5_computed_ = md5_receive_str.get();
md5_receive.get_hex(md5_receive_str);
this->md5_computed_ = md5_receive_str;
if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
this->cleanup_(std::move(backend), container);
return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
} else {
backend->set_update_md5(md5_receive_str.get());
backend->set_update_md5(md5_receive_str);
}
container->end();

View File

@@ -191,10 +191,17 @@ def _notify_old_style(config):
# The dev and latest branches will be at *least* this version, which is what matters.
# Use GitHub releases directly to avoid PlatformIO moderation delays.
ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 9, 2), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (cv.Version(1, 9, 2), "libretiny"),
"recommended": (cv.Version(1, 9, 2), None),
"dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (
cv.Version(1, 11, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
),
"recommended": (
cv.Version(1, 11, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
),
}

View File

@@ -7,9 +7,7 @@ namespace esphome::light {
class AddressableLightWrapper : public light::AddressableLight {
public:
explicit AddressableLightWrapper(light::LightState *light_state) : light_state_(light_state) {
this->wrapper_state_ = new uint8_t[5]; // NOLINT(cppcoreguidelines-owning-memory)
}
explicit AddressableLightWrapper(light::LightState *light_state) : light_state_(light_state) {}
int32_t size() const override { return 1; }
@@ -118,7 +116,7 @@ class AddressableLightWrapper : public light::AddressableLight {
}
light::LightState *light_state_;
uint8_t *wrapper_state_;
mutable uint8_t wrapper_state_[5]{};
ColorMode color_mode_{ColorMode::UNKNOWN};
};

View File

@@ -3,8 +3,7 @@
#include <cinttypes>
namespace esphome {
namespace mhz19 {
namespace esphome::mhz19 {
static const char *const TAG = "mhz19";
static const uint8_t MHZ19_REQUEST_LENGTH = 8;
@@ -17,6 +16,19 @@ static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM[] = {0xFF, 0x01, 0x
static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x13, 0x88};
static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x27, 0x10};
static const LogString *detection_range_to_log_string(MHZ19DetectionRange range) {
switch (range) {
case MHZ19_DETECTION_RANGE_0_2000PPM:
return LOG_STR("0-2000 ppm");
case MHZ19_DETECTION_RANGE_0_5000PPM:
return LOG_STR("0-5000 ppm");
case MHZ19_DETECTION_RANGE_0_10000PPM:
return LOG_STR("0-10000 ppm");
default:
return LOG_STR("default");
}
}
uint8_t mhz19_checksum(const uint8_t *command) {
uint8_t sum = 0;
for (uint8_t i = 1; i < MHZ19_REQUEST_LENGTH; i++) {
@@ -91,24 +103,24 @@ void MHZ19Component::abc_disable() {
this->mhz19_write_command_(MHZ19_COMMAND_ABC_DISABLE, nullptr);
}
void MHZ19Component::range_set(MHZ19DetectionRange detection_ppm) {
switch (detection_ppm) {
case MHZ19_DETECTION_RANGE_DEFAULT:
ESP_LOGV(TAG, "Using previously set detection range (no change)");
break;
void MHZ19Component::range_set(MHZ19DetectionRange detection_range) {
const uint8_t *command;
switch (detection_range) {
case MHZ19_DETECTION_RANGE_0_2000PPM:
ESP_LOGD(TAG, "Setting detection range to 0 to 2000ppm");
this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM, nullptr);
command = MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM;
break;
case MHZ19_DETECTION_RANGE_0_5000PPM:
ESP_LOGD(TAG, "Setting detection range to 0 to 5000ppm");
this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM, nullptr);
command = MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM;
break;
case MHZ19_DETECTION_RANGE_0_10000PPM:
ESP_LOGD(TAG, "Setting detection range to 0 to 10000ppm");
this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM, nullptr);
command = MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM;
break;
default:
ESP_LOGV(TAG, "Using previously set detection range (no change)");
return;
}
ESP_LOGD(TAG, "Setting detection range to %s", LOG_STR_ARG(detection_range_to_log_string(detection_range)));
this->mhz19_write_command_(command, nullptr);
}
bool MHZ19Component::mhz19_write_command_(const uint8_t *command, uint8_t *response) {
@@ -140,24 +152,7 @@ void MHZ19Component::dump_config() {
}
ESP_LOGCONFIG(TAG, " Warmup time: %" PRIu32 " s", this->warmup_seconds_);
const char *range_str;
switch (this->detection_range_) {
case MHZ19_DETECTION_RANGE_DEFAULT:
range_str = "default";
break;
case MHZ19_DETECTION_RANGE_0_2000PPM:
range_str = "0 to 2000ppm";
break;
case MHZ19_DETECTION_RANGE_0_5000PPM:
range_str = "0 to 5000ppm";
break;
case MHZ19_DETECTION_RANGE_0_10000PPM:
range_str = "0 to 10000ppm";
break;
}
ESP_LOGCONFIG(TAG, " Detection range: %s", range_str);
ESP_LOGCONFIG(TAG, " Detection range: %s", LOG_STR_ARG(detection_range_to_log_string(this->detection_range_)));
}
} // namespace mhz19
} // namespace esphome
} // namespace esphome::mhz19

View File

@@ -5,8 +5,7 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace mhz19 {
namespace esphome::mhz19 {
enum MHZ19ABCLogic {
MHZ19_ABC_NONE = 0,
@@ -32,7 +31,7 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice {
void calibrate_zero();
void abc_enable();
void abc_disable();
void range_set(MHZ19DetectionRange detection_ppm);
void range_set(MHZ19DetectionRange detection_range);
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; }
@@ -74,5 +73,4 @@ template<typename... Ts> class MHZ19DetectionRangeSetAction : public Action<Ts..
void play(const Ts &...x) override { this->parent_->range_set(this->detection_range_.value(x...)); }
};
} // namespace mhz19
} // namespace esphome
} // namespace esphome::mhz19

View File

@@ -1,5 +1,6 @@
#include "mqtt_alarm_control_panel.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "mqtt_const.h"
@@ -12,6 +13,33 @@ static const char *const TAG = "mqtt.alarm_control_panel";
using namespace esphome::alarm_control_panel;
static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) {
switch (state) {
case ACP_STATE_DISARMED:
return ESPHOME_F("disarmed");
case ACP_STATE_ARMED_HOME:
return ESPHOME_F("armed_home");
case ACP_STATE_ARMED_AWAY:
return ESPHOME_F("armed_away");
case ACP_STATE_ARMED_NIGHT:
return ESPHOME_F("armed_night");
case ACP_STATE_ARMED_VACATION:
return ESPHOME_F("armed_vacation");
case ACP_STATE_ARMED_CUSTOM_BYPASS:
return ESPHOME_F("armed_custom_bypass");
case ACP_STATE_PENDING:
return ESPHOME_F("pending");
case ACP_STATE_ARMING:
return ESPHOME_F("arming");
case ACP_STATE_DISARMING:
return ESPHOME_F("disarming");
case ACP_STATE_TRIGGERED:
return ESPHOME_F("triggered");
default:
return ESPHOME_F("unknown");
}
}
MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel)
: alarm_control_panel_(alarm_control_panel) {}
void MQTTAlarmControlPanelComponent::setup() {
@@ -84,42 +112,9 @@ const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return th
bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); }
bool MQTTAlarmControlPanelComponent::publish_state() {
const char *state_s;
switch (this->alarm_control_panel_->get_state()) {
case ACP_STATE_DISARMED:
state_s = "disarmed";
break;
case ACP_STATE_ARMED_HOME:
state_s = "armed_home";
break;
case ACP_STATE_ARMED_AWAY:
state_s = "armed_away";
break;
case ACP_STATE_ARMED_NIGHT:
state_s = "armed_night";
break;
case ACP_STATE_ARMED_VACATION:
state_s = "armed_vacation";
break;
case ACP_STATE_ARMED_CUSTOM_BYPASS:
state_s = "armed_custom_bypass";
break;
case ACP_STATE_PENDING:
state_s = "pending";
break;
case ACP_STATE_ARMING:
state_s = "arming";
break;
case ACP_STATE_DISARMING:
state_s = "disarming";
break;
case ACP_STATE_TRIGGERED:
state_s = "triggered";
break;
default:
state_s = "unknown";
}
return this->publish(this->get_state_topic_(), state_s);
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish(this->get_state_topic_to_(topic_buf),
alarm_state_to_mqtt_str(this->alarm_control_panel_->get_state()));
}
} // namespace esphome::mqtt

View File

@@ -52,8 +52,9 @@ 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_(), state_s);
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
}
} // namespace esphome::mqtt

View File

@@ -1,5 +1,6 @@
#include "mqtt_climate.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "mqtt_const.h"
@@ -12,6 +13,111 @@ static const char *const TAG = "mqtt.climate";
using namespace esphome::climate;
static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) {
switch (mode) {
case CLIMATE_MODE_OFF:
return ESPHOME_F("off");
case CLIMATE_MODE_HEAT_COOL:
return ESPHOME_F("heat_cool");
case CLIMATE_MODE_AUTO:
return ESPHOME_F("auto");
case CLIMATE_MODE_COOL:
return ESPHOME_F("cool");
case CLIMATE_MODE_HEAT:
return ESPHOME_F("heat");
case CLIMATE_MODE_FAN_ONLY:
return ESPHOME_F("fan_only");
case CLIMATE_MODE_DRY:
return ESPHOME_F("dry");
default:
return ESPHOME_F("unknown");
}
}
static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) {
switch (action) {
case CLIMATE_ACTION_OFF:
return ESPHOME_F("off");
case CLIMATE_ACTION_COOLING:
return ESPHOME_F("cooling");
case CLIMATE_ACTION_HEATING:
return ESPHOME_F("heating");
case CLIMATE_ACTION_IDLE:
return ESPHOME_F("idle");
case CLIMATE_ACTION_DRYING:
return ESPHOME_F("drying");
case CLIMATE_ACTION_FAN:
return ESPHOME_F("fan");
default:
return ESPHOME_F("unknown");
}
}
static ProgmemStr climate_fan_mode_to_mqtt_str(ClimateFanMode fan_mode) {
switch (fan_mode) {
case CLIMATE_FAN_ON:
return ESPHOME_F("on");
case CLIMATE_FAN_OFF:
return ESPHOME_F("off");
case CLIMATE_FAN_AUTO:
return ESPHOME_F("auto");
case CLIMATE_FAN_LOW:
return ESPHOME_F("low");
case CLIMATE_FAN_MEDIUM:
return ESPHOME_F("medium");
case CLIMATE_FAN_HIGH:
return ESPHOME_F("high");
case CLIMATE_FAN_MIDDLE:
return ESPHOME_F("middle");
case CLIMATE_FAN_FOCUS:
return ESPHOME_F("focus");
case CLIMATE_FAN_DIFFUSE:
return ESPHOME_F("diffuse");
case CLIMATE_FAN_QUIET:
return ESPHOME_F("quiet");
default:
return ESPHOME_F("unknown");
}
}
static ProgmemStr climate_swing_mode_to_mqtt_str(ClimateSwingMode swing_mode) {
switch (swing_mode) {
case CLIMATE_SWING_OFF:
return ESPHOME_F("off");
case CLIMATE_SWING_BOTH:
return ESPHOME_F("both");
case CLIMATE_SWING_VERTICAL:
return ESPHOME_F("vertical");
case CLIMATE_SWING_HORIZONTAL:
return ESPHOME_F("horizontal");
default:
return ESPHOME_F("unknown");
}
}
static ProgmemStr climate_preset_to_mqtt_str(ClimatePreset preset) {
switch (preset) {
case CLIMATE_PRESET_NONE:
return ESPHOME_F("none");
case CLIMATE_PRESET_HOME:
return ESPHOME_F("home");
case CLIMATE_PRESET_ECO:
return ESPHOME_F("eco");
case CLIMATE_PRESET_AWAY:
return ESPHOME_F("away");
case CLIMATE_PRESET_BOOST:
return ESPHOME_F("boost");
case CLIMATE_PRESET_COMFORT:
return ESPHOME_F("comfort");
case CLIMATE_PRESET_SLEEP:
return ESPHOME_F("sleep");
case CLIMATE_PRESET_ACTIVITY:
return ESPHOME_F("activity");
default:
return ESPHOME_F("unknown");
}
}
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
auto traits = this->device_->get_traits();
@@ -260,34 +366,8 @@ const EntityBase *MQTTClimateComponent::get_entity() const { return this->device
bool MQTTClimateComponent::publish_state_() {
auto traits = this->device_->get_traits();
// mode
const char *mode_s;
switch (this->device_->mode) {
case CLIMATE_MODE_OFF:
mode_s = "off";
break;
case CLIMATE_MODE_AUTO:
mode_s = "auto";
break;
case CLIMATE_MODE_COOL:
mode_s = "cool";
break;
case CLIMATE_MODE_HEAT:
mode_s = "heat";
break;
case CLIMATE_MODE_FAN_ONLY:
mode_s = "fan_only";
break;
case CLIMATE_MODE_DRY:
mode_s = "dry";
break;
case CLIMATE_MODE_HEAT_COOL:
mode_s = "heat_cool";
break;
default:
mode_s = "unknown";
}
bool success = true;
if (!this->publish(this->get_mode_state_topic(), mode_s))
if (!this->publish(this->get_mode_state_topic(), climate_mode_to_mqtt_str(this->device_->mode)))
success = false;
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
@@ -327,134 +407,37 @@ bool MQTTClimateComponent::publish_state_() {
}
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
std::string payload;
if (this->device_->preset.has_value()) {
switch (this->device_->preset.value()) {
case CLIMATE_PRESET_NONE:
payload = "none";
break;
case CLIMATE_PRESET_HOME:
payload = "home";
break;
case CLIMATE_PRESET_AWAY:
payload = "away";
break;
case CLIMATE_PRESET_BOOST:
payload = "boost";
break;
case CLIMATE_PRESET_COMFORT:
payload = "comfort";
break;
case CLIMATE_PRESET_ECO:
payload = "eco";
break;
case CLIMATE_PRESET_SLEEP:
payload = "sleep";
break;
case CLIMATE_PRESET_ACTIVITY:
payload = "activity";
break;
default:
payload = "unknown";
}
}
if (this->device_->has_custom_preset())
payload = this->device_->get_custom_preset().c_str();
if (!this->publish(this->get_preset_state_topic(), payload))
if (this->device_->has_custom_preset()) {
if (!this->publish(this->get_preset_state_topic(), this->device_->get_custom_preset()))
success = false;
} else if (this->device_->preset.has_value()) {
if (!this->publish(this->get_preset_state_topic(), climate_preset_to_mqtt_str(this->device_->preset.value())))
success = false;
} else if (!this->publish(this->get_preset_state_topic(), "")) {
success = false;
}
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
const char *payload;
switch (this->device_->action) {
case CLIMATE_ACTION_OFF:
payload = "off";
break;
case CLIMATE_ACTION_COOLING:
payload = "cooling";
break;
case CLIMATE_ACTION_HEATING:
payload = "heating";
break;
case CLIMATE_ACTION_IDLE:
payload = "idle";
break;
case CLIMATE_ACTION_DRYING:
payload = "drying";
break;
case CLIMATE_ACTION_FAN:
payload = "fan";
break;
default:
payload = "unknown";
}
if (!this->publish(this->get_action_state_topic(), payload))
if (!this->publish(this->get_action_state_topic(), climate_action_to_mqtt_str(this->device_->action)))
success = false;
}
if (traits.get_supports_fan_modes()) {
std::string payload;
if (this->device_->fan_mode.has_value()) {
switch (this->device_->fan_mode.value()) {
case CLIMATE_FAN_ON:
payload = "on";
break;
case CLIMATE_FAN_OFF:
payload = "off";
break;
case CLIMATE_FAN_AUTO:
payload = "auto";
break;
case CLIMATE_FAN_LOW:
payload = "low";
break;
case CLIMATE_FAN_MEDIUM:
payload = "medium";
break;
case CLIMATE_FAN_HIGH:
payload = "high";
break;
case CLIMATE_FAN_MIDDLE:
payload = "middle";
break;
case CLIMATE_FAN_FOCUS:
payload = "focus";
break;
case CLIMATE_FAN_DIFFUSE:
payload = "diffuse";
break;
case CLIMATE_FAN_QUIET:
payload = "quiet";
break;
default:
payload = "unknown";
}
}
if (this->device_->has_custom_fan_mode())
payload = this->device_->get_custom_fan_mode().c_str();
if (!this->publish(this->get_fan_mode_state_topic(), payload))
if (this->device_->has_custom_fan_mode()) {
if (!this->publish(this->get_fan_mode_state_topic(), this->device_->get_custom_fan_mode()))
success = false;
} else if (this->device_->fan_mode.has_value()) {
if (!this->publish(this->get_fan_mode_state_topic(),
climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value())))
success = false;
} else if (!this->publish(this->get_fan_mode_state_topic(), "")) {
success = false;
}
}
if (traits.get_supports_swing_modes()) {
const char *payload;
switch (this->device_->swing_mode) {
case CLIMATE_SWING_OFF:
payload = "off";
break;
case CLIMATE_SWING_BOTH:
payload = "both";
break;
case CLIMATE_SWING_VERTICAL:
payload = "vertical";
break;
case CLIMATE_SWING_HORIZONTAL:
payload = "horizontal";
break;
default:
payload = "unknown";
}
if (!this->publish(this->get_swing_mode_state_topic(), payload))
if (!this->publish(this->get_swing_mode_state_topic(), climate_swing_mode_to_mqtt_str(this->device_->swing_mode)))
success = false;
}

View File

@@ -5,6 +5,7 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/version.h"
#include "mqtt_const.h"
@@ -132,17 +133,45 @@ std::string MQTTComponent::get_command_topic_() const {
}
bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
return this->publish(topic, payload.data(), payload.size());
return this->publish(topic.c_str(), payload.data(), payload.size());
}
bool MQTTComponent::publish(const std::string &topic, const char *payload, size_t payload_length) {
if (topic.empty())
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')
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));
}
#ifdef USE_ESP8266
bool MQTTComponent::publish(const std::string &topic, ProgmemStr payload) {
return this->publish(topic.c_str(), payload);
}
bool MQTTComponent::publish(const char *topic, ProgmemStr payload) {
if (topic[0] == '\0')
return false;
// On ESP8266, ProgmemStr is __FlashStringHelper* - need to copy from flash
char buf[64];
strncpy_P(buf, reinterpret_cast<const char *>(payload), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
return global_mqtt_client->publish(topic, buf, strlen(buf), this->qos_, this->retain_);
}
#endif
bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) {
if (topic.empty())
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')
return false;
return global_mqtt_client->publish_json(topic, f, this->qos_, this->retain_);
}

View File

@@ -9,6 +9,7 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
#include "mqtt_client.h"
@@ -157,6 +158,70 @@ class MQTTComponent : public Component {
*/
bool publish(const std::string &topic, const char *payload, size_t payload_length);
/** Send a MQTT message.
*
* @param topic The topic.
* @param payload The null-terminated payload.
*/
bool publish(const std::string &topic, const char *payload) {
return this->publish(topic.c_str(), payload, strlen(payload));
}
/** 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); }
#ifdef USE_ESP8266
/** Send a MQTT message with a PROGMEM string payload.
*
* @param topic The topic.
* @param payload The payload (ProgmemStr - stored in flash on ESP8266).
*/
bool publish(const std::string &topic, ProgmemStr payload);
/** Send a MQTT message with a PROGMEM string payload (no heap allocation for topic).
*
* @param topic The topic as C string.
* @param payload The payload (ProgmemStr - stored in flash on ESP8266).
*/
bool publish(const char *topic, ProgmemStr payload);
/** Send a MQTT message with a PROGMEM string payload (no heap allocation for topic).
*
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
* @param payload The payload (ProgmemStr - stored in flash on ESP8266).
*/
bool publish(StringRef topic, ProgmemStr payload) { return this->publish(topic.c_str(), payload); }
#endif
/** Construct and send a JSON MQTT message.
*
* @param topic The topic.
@@ -164,6 +229,20 @@ 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

@@ -1,5 +1,6 @@
#include "mqtt_cover.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "mqtt_const.h"
@@ -12,6 +13,20 @@ static const char *const TAG = "mqtt.cover";
using namespace esphome::cover;
static ProgmemStr cover_state_to_mqtt_str(CoverOperation operation, float position, bool supports_position) {
if (operation == COVER_OPERATION_OPENING)
return ESPHOME_F("opening");
if (operation == COVER_OPERATION_CLOSING)
return ESPHOME_F("closing");
if (position == COVER_CLOSED)
return ESPHOME_F("closed");
if (position == COVER_OPEN)
return ESPHOME_F("open");
if (supports_position)
return ESPHOME_F("open");
return ESPHOME_F("unknown");
}
MQTTCoverComponent::MQTTCoverComponent(Cover *cover) : cover_(cover) {}
void MQTTCoverComponent::setup() {
auto traits = this->cover_->get_traits();
@@ -109,13 +124,10 @@ bool MQTTCoverComponent::publish_state() {
if (!this->publish(this->get_tilt_state_topic(), pos, len))
success = false;
}
const char *state_s = this->cover_->current_operation == COVER_OPERATION_OPENING ? "opening"
: this->cover_->current_operation == COVER_OPERATION_CLOSING ? "closing"
: this->cover_->position == COVER_CLOSED ? "closed"
: this->cover_->position == COVER_OPEN ? "open"
: traits.get_supports_position() ? "open"
: "unknown";
if (!this->publish(this->get_state_topic_(), state_s))
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf),
cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
traits.get_supports_position())))
success = false;
return success;
}

View File

@@ -53,7 +53,8 @@ bool MQTTDateComponent::send_initial_state() {
}
}
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) {
return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [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,15 +66,17 @@ 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) {
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;
});
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;
});
}
} // namespace esphome::mqtt

View File

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

View File

@@ -1,5 +1,6 @@
#include "mqtt_fan.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "mqtt_const.h"
@@ -12,6 +13,14 @@ static const char *const TAG = "mqtt.fan";
using namespace esphome::fan;
static ProgmemStr fan_direction_to_mqtt_str(FanDirection direction) {
return direction == FanDirection::FORWARD ? ESPHOME_F("forward") : ESPHOME_F("reverse");
}
static ProgmemStr fan_oscillation_to_mqtt_str(bool oscillating) {
return oscillating ? ESPHOME_F("oscillate_on") : ESPHOME_F("oscillate_off");
}
MQTTFanComponent::MQTTFanComponent(Fan *state) : state_(state) {}
Fan *MQTTFanComponent::get_state() const { return this->state_; }
@@ -158,18 +167,18 @@ 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_(), state_s);
this->publish(this->get_state_topic_to_(topic_buf), state_s);
bool failed = false;
if (this->state_->get_traits().supports_direction()) {
bool success = this->publish(this->get_direction_state_topic(),
this->state_->direction == fan::FanDirection::FORWARD ? "forward" : "reverse");
bool success = this->publish(this->get_direction_state_topic(), fan_direction_to_mqtt_str(this->state_->direction));
failed = failed || !success;
}
if (this->state_->get_traits().supports_oscillation()) {
bool success = this->publish(this->get_oscillation_state_topic(),
this->state_->oscillating ? "oscillate_on" : "oscillate_off");
bool success =
this->publish(this->get_oscillation_state_topic(), fan_oscillation_to_mqtt_str(this->state_->oscillating));
failed = failed || !success;
}
auto traits = this->state_->get_traits();

View File

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

View File

@@ -47,13 +47,14 @@ 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_(), buf);
return this->publish(this->get_state_topic_to_(topic_buf), buf);
#else
return this->publish(this->get_state_topic_(), LOG_STR_ARG(lock_state_to_string(this->lock_->state)));
return this->publish(this->get_state_topic_to_(topic_buf), LOG_STR_ARG(lock_state_to_string(this->lock_->state)));
#endif
}

View File

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

View File

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

View File

@@ -79,12 +79,13 @@ 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_(), "None", 4);
return this->publish(this->get_state_topic_to_(topic_buf), "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_(), buf, len);
return this->publish(this->get_state_topic_to_(topic_buf), buf, len);
}
} // namespace esphome::mqtt

View File

@@ -52,8 +52,9 @@ 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_(), state_s);
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
}
} // namespace esphome::mqtt

View File

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

View File

@@ -31,7 +31,10 @@ void MQTTTextSensor::dump_config() {
LOG_MQTT_COMPONENT(true, false);
}
bool MQTTTextSensor::publish_state(const std::string &value) { return this->publish(this->get_state_topic_(), value); }
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::send_initial_state() {
if (this->sensor_->has_state()) {
return this->publish_state(this->sensor_->state);

View File

@@ -53,7 +53,8 @@ bool MQTTTimeComponent::send_initial_state() {
}
}
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) {
return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [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,7 +28,8 @@ void MQTTUpdateComponent::setup() {
}
bool MQTTUpdateComponent::publish_state() {
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [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

@@ -1,5 +1,6 @@
#include "mqtt_valve.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "mqtt_const.h"
@@ -12,6 +13,20 @@ static const char *const TAG = "mqtt.valve";
using namespace esphome::valve;
static ProgmemStr valve_state_to_mqtt_str(ValveOperation operation, float position, bool supports_position) {
if (operation == VALVE_OPERATION_OPENING)
return ESPHOME_F("opening");
if (operation == VALVE_OPERATION_CLOSING)
return ESPHOME_F("closing");
if (position == VALVE_CLOSED)
return ESPHOME_F("closed");
if (position == VALVE_OPEN)
return ESPHOME_F("open");
if (supports_position)
return ESPHOME_F("open");
return ESPHOME_F("unknown");
}
MQTTValveComponent::MQTTValveComponent(Valve *valve) : valve_(valve) {}
void MQTTValveComponent::setup() {
auto traits = this->valve_->get_traits();
@@ -78,13 +93,10 @@ bool MQTTValveComponent::publish_state() {
if (!this->publish(this->get_position_state_topic(), pos, len))
success = false;
}
const char *state_s = this->valve_->current_operation == VALVE_OPERATION_OPENING ? "opening"
: this->valve_->current_operation == VALVE_OPERATION_CLOSING ? "closing"
: this->valve_->position == VALVE_CLOSED ? "closed"
: this->valve_->position == VALVE_OPEN ? "open"
: traits.get_supports_position() ? "open"
: "unknown";
if (!this->publish(this->get_state_topic_(), state_s))
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf),
valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position,
traits.get_supports_position())))
success = false;
return success;
}

View File

@@ -40,7 +40,7 @@ void NfcTagBinarySensor::set_tag_name(const std::string &str) {
this->match_tag_name_ = true;
}
void NfcTagBinarySensor::set_uid(const NfcTagUid &uid) { this->uid_ = uid; }
void NfcTagBinarySensor::set_uid(const std::vector<uint8_t> &uid) { this->uid_ = uid; }
bool NfcTagBinarySensor::tag_match_ndef_string(const std::shared_ptr<NdefMessage> &msg) {
for (const auto &record : msg->get_records()) {
@@ -63,7 +63,7 @@ bool NfcTagBinarySensor::tag_match_tag_name(const std::shared_ptr<NdefMessage> &
return false;
}
bool NfcTagBinarySensor::tag_match_uid(const NfcTagUid &data) {
bool NfcTagBinarySensor::tag_match_uid(const std::vector<uint8_t> &data) {
if (data.size() != this->uid_.size()) {
return false;
}

View File

@@ -19,11 +19,11 @@ class NfcTagBinarySensor : public binary_sensor::BinarySensor,
void set_ndef_match_string(const std::string &str);
void set_tag_name(const std::string &str);
void set_uid(const NfcTagUid &uid);
void set_uid(const std::vector<uint8_t> &uid);
bool tag_match_ndef_string(const std::shared_ptr<NdefMessage> &msg);
bool tag_match_tag_name(const std::shared_ptr<NdefMessage> &msg);
bool tag_match_uid(const NfcTagUid &data);
bool tag_match_uid(const std::vector<uint8_t> &data);
void tag_off(NfcTag &tag) override;
void tag_on(NfcTag &tag) override;
@@ -31,7 +31,7 @@ class NfcTagBinarySensor : public binary_sensor::BinarySensor,
protected:
bool match_tag_name_{false};
std::string match_string_;
NfcTagUid uid_;
std::vector<uint8_t> uid_;
};
} // namespace nfc

View File

@@ -8,23 +8,19 @@ namespace nfc {
static const char *const TAG = "nfc";
char *format_uid_to(char *buffer, std::span<const uint8_t> uid) {
char *format_uid_to(char *buffer, const std::vector<uint8_t> &uid) {
return format_hex_pretty_to(buffer, FORMAT_UID_BUFFER_SIZE, uid.data(), uid.size(), '-');
}
char *format_bytes_to(char *buffer, std::span<const uint8_t> bytes) {
char *format_bytes_to(char *buffer, const std::vector<uint8_t> &bytes) {
return format_hex_pretty_to(buffer, FORMAT_BYTES_BUFFER_SIZE, bytes.data(), bytes.size(), ' ');
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
// Deprecated wrappers intentionally use heap-allocating version for backward compatibility
std::string format_uid(std::span<const uint8_t> uid) {
return format_hex_pretty(uid.data(), uid.size(), '-', false); // NOLINT
}
std::string format_bytes(std::span<const uint8_t> bytes) {
return format_hex_pretty(bytes.data(), bytes.size(), ' ', false); // NOLINT
}
std::string format_uid(const std::vector<uint8_t> &uid) { return format_hex_pretty(uid, '-', false); } // NOLINT
std::string format_bytes(const std::vector<uint8_t> &bytes) { return format_hex_pretty(bytes, ' ', false); } // NOLINT
#pragma GCC diagnostic pop
uint8_t guess_tag_type(uint8_t uid_length) {

View File

@@ -6,7 +6,6 @@
#include "ndef_record.h"
#include "nfc_tag.h"
#include <span>
#include <vector>
namespace esphome {
@@ -57,19 +56,19 @@ static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5};
/// Max UID size is 10 bytes, formatted as "XX-XX-XX-XX-XX-XX-XX-XX-XX-XX\0" = 30 chars
static constexpr size_t FORMAT_UID_BUFFER_SIZE = 30;
/// Format UID to buffer with '-' separator (e.g., "04-11-22-33"). Returns buffer for inline use.
char *format_uid_to(char *buffer, std::span<const uint8_t> uid);
char *format_uid_to(char *buffer, const std::vector<uint8_t> &uid);
/// Buffer size for format_bytes_to (64 bytes max = 192 chars with space separator)
static constexpr size_t FORMAT_BYTES_BUFFER_SIZE = 192;
/// Format bytes to buffer with ' ' separator (e.g., "04 11 22 33"). Returns buffer for inline use.
char *format_bytes_to(char *buffer, std::span<const uint8_t> bytes);
char *format_bytes_to(char *buffer, const std::vector<uint8_t> &bytes);
// Remove before 2026.6.0
ESPDEPRECATED("Use format_uid_to() with stack buffer instead. Removed in 2026.6.0", "2025.12.0")
std::string format_uid(std::span<const uint8_t> uid);
std::string format_uid(const std::vector<uint8_t> &uid);
// Remove before 2026.6.0
ESPDEPRECATED("Use format_bytes_to() with stack buffer instead. Removed in 2026.6.0", "2025.12.0")
std::string format_bytes(std::span<const uint8_t> bytes);
std::string format_bytes(const std::vector<uint8_t> &bytes);
uint8_t guess_tag_type(uint8_t uid_length);
uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data);

View File

@@ -10,27 +10,26 @@
namespace esphome {
namespace nfc {
// NFC UIDs are 4, 7, or 10 bytes depending on tag type
static constexpr size_t NFC_UID_MAX_LENGTH = 10;
using NfcTagUid = StaticVector<uint8_t, NFC_UID_MAX_LENGTH>;
class NfcTag {
public:
NfcTag() { this->tag_type_ = "Unknown"; };
NfcTag(const NfcTagUid &uid) {
NfcTag() {
this->uid_ = {};
this->tag_type_ = "Unknown";
};
NfcTag(std::vector<uint8_t> &uid) {
this->uid_ = uid;
this->tag_type_ = "Unknown";
};
NfcTag(const NfcTagUid &uid, const std::string &tag_type) {
NfcTag(std::vector<uint8_t> &uid, const std::string &tag_type) {
this->uid_ = uid;
this->tag_type_ = tag_type;
};
NfcTag(const NfcTagUid &uid, const std::string &tag_type, std::unique_ptr<nfc::NdefMessage> ndef_message) {
NfcTag(std::vector<uint8_t> &uid, const std::string &tag_type, std::unique_ptr<nfc::NdefMessage> ndef_message) {
this->uid_ = uid;
this->tag_type_ = tag_type;
this->ndef_message_ = std::move(ndef_message);
};
NfcTag(const NfcTagUid &uid, const std::string &tag_type, std::vector<uint8_t> &ndef_data) {
NfcTag(std::vector<uint8_t> &uid, const std::string &tag_type, std::vector<uint8_t> &ndef_data) {
this->uid_ = uid;
this->tag_type_ = tag_type;
this->ndef_message_ = make_unique<NdefMessage>(ndef_data);
@@ -42,14 +41,14 @@ class NfcTag {
ndef_message_ = make_unique<NdefMessage>(*rhs.ndef_message_);
}
NfcTagUid &get_uid() { return this->uid_; };
std::vector<uint8_t> &get_uid() { return this->uid_; };
const std::string &get_tag_type() { return this->tag_type_; };
bool has_ndef_message() { return this->ndef_message_ != nullptr; };
const std::shared_ptr<NdefMessage> &get_ndef_message() { return this->ndef_message_; };
void set_ndef_message(std::unique_ptr<NdefMessage> ndef_message) { this->ndef_message_ = std::move(ndef_message); };
protected:
NfcTagUid uid_;
std::vector<uint8_t> uid_;
std::string tag_type_;
std::shared_ptr<NdefMessage> ndef_message_;
};

View File

@@ -168,11 +168,11 @@ void PN532::loop() {
}
uint8_t nfcid_length = read[5];
if (nfcid_length > nfc::NFC_UID_MAX_LENGTH || read.size() < 6U + nfcid_length) {
std::vector<uint8_t> nfcid(read.begin() + 6, read.begin() + 6 + nfcid_length);
if (read.size() < 6U + nfcid_length) {
// oops, pn532 returned invalid data
return;
}
nfc::NfcTagUid nfcid(read.begin() + 6, read.begin() + 6 + nfcid_length);
bool report = true;
for (auto *bin_sens : this->binary_sensors_) {
@@ -358,7 +358,7 @@ void PN532::turn_off_rf_() {
});
}
std::unique_ptr<nfc::NfcTag> PN532::read_tag_(nfc::NfcTagUid &uid) {
std::unique_ptr<nfc::NfcTag> PN532::read_tag_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
@@ -393,7 +393,7 @@ void PN532::write_mode(nfc::NdefMessage *message) {
ESP_LOGD(TAG, "Waiting to write next tag");
}
bool PN532::clean_tag_(nfc::NfcTagUid &uid) {
bool PN532::clean_tag_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
return this->format_mifare_classic_mifare_(uid);
@@ -404,7 +404,7 @@ bool PN532::clean_tag_(nfc::NfcTagUid &uid) {
return false;
}
bool PN532::format_tag_(nfc::NfcTagUid &uid) {
bool PN532::format_tag_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
return this->format_mifare_classic_ndef_(uid);
@@ -415,7 +415,7 @@ bool PN532::format_tag_(nfc::NfcTagUid &uid) {
return false;
}
bool PN532::write_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) {
bool PN532::write_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message) {
uint8_t type = nfc::guess_tag_type(uid.size());
if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
return this->write_mifare_classic_tag_(uid, message);
@@ -448,7 +448,7 @@ void PN532::dump_config() {
}
}
bool PN532BinarySensor::process(const nfc::NfcTagUid &data) {
bool PN532BinarySensor::process(std::vector<uint8_t> &data) {
if (data.size() != this->uid_.size())
return false;

View File

@@ -69,28 +69,28 @@ class PN532 : public PollingComponent {
virtual bool read_data(std::vector<uint8_t> &data, uint8_t len) = 0;
virtual bool read_response(uint8_t command, std::vector<uint8_t> &data) = 0;
std::unique_ptr<nfc::NfcTag> read_tag_(nfc::NfcTagUid &uid);
std::unique_ptr<nfc::NfcTag> read_tag_(std::vector<uint8_t> &uid);
bool format_tag_(nfc::NfcTagUid &uid);
bool clean_tag_(nfc::NfcTagUid &uid);
bool write_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
bool format_tag_(std::vector<uint8_t> &uid);
bool clean_tag_(std::vector<uint8_t> &uid);
bool write_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message);
std::unique_ptr<nfc::NfcTag> read_mifare_classic_tag_(nfc::NfcTagUid &uid);
std::unique_ptr<nfc::NfcTag> read_mifare_classic_tag_(std::vector<uint8_t> &uid);
bool read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
bool write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
bool auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key);
bool format_mifare_classic_mifare_(nfc::NfcTagUid &uid);
bool format_mifare_classic_ndef_(nfc::NfcTagUid &uid);
bool write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
bool auth_mifare_classic_block_(std::vector<uint8_t> &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key);
bool format_mifare_classic_mifare_(std::vector<uint8_t> &uid);
bool format_mifare_classic_ndef_(std::vector<uint8_t> &uid);
bool write_mifare_classic_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message);
std::unique_ptr<nfc::NfcTag> read_mifare_ultralight_tag_(nfc::NfcTagUid &uid);
std::unique_ptr<nfc::NfcTag> read_mifare_ultralight_tag_(std::vector<uint8_t> &uid);
bool read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector<uint8_t> &data);
bool is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_to_6);
uint16_t read_mifare_ultralight_capacity_();
bool find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index);
bool write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
bool write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message);
bool clean_mifare_ultralight_();
bool updates_enabled_{true};
@@ -98,7 +98,7 @@ class PN532 : public PollingComponent {
std::vector<PN532BinarySensor *> binary_sensors_;
std::vector<nfc::NfcOnTagTrigger *> triggers_ontag_;
std::vector<nfc::NfcOnTagTrigger *> triggers_ontagremoved_;
nfc::NfcTagUid current_uid_;
std::vector<uint8_t> current_uid_;
nfc::NdefMessage *next_task_message_to_write_;
uint32_t rd_start_time_{0};
enum PN532ReadReady rd_ready_ { WOULDBLOCK };
@@ -118,9 +118,9 @@ class PN532 : public PollingComponent {
class PN532BinarySensor : public binary_sensor::BinarySensor {
public:
void set_uid(const nfc::NfcTagUid &uid) { uid_ = uid; }
void set_uid(const std::vector<uint8_t> &uid) { uid_ = uid; }
bool process(const nfc::NfcTagUid &data);
bool process(std::vector<uint8_t> &data);
void on_scan_end() {
if (!this->found_) {
@@ -130,7 +130,7 @@ class PN532BinarySensor : public binary_sensor::BinarySensor {
}
protected:
nfc::NfcTagUid uid_;
std::vector<uint8_t> uid_;
bool found_{false};
};

View File

@@ -8,7 +8,7 @@ namespace pn532 {
static const char *const TAG = "pn532.mifare_classic";
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_classic_tag_(nfc::NfcTagUid &uid) {
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_classic_tag_(std::vector<uint8_t> &uid) {
uint8_t current_block = 4;
uint8_t message_start_index = 0;
uint32_t message_length = 0;
@@ -82,7 +82,8 @@ bool PN532::read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &
return true;
}
bool PN532::auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key) {
bool PN532::auth_mifare_classic_block_(std::vector<uint8_t> &uid, uint8_t block_num, uint8_t key_num,
const uint8_t *key) {
std::vector<uint8_t> data({
PN532_COMMAND_INDATAEXCHANGE,
0x01, // One card
@@ -105,7 +106,7 @@ bool PN532::auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, u
return true;
}
bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
bool PN532::format_mifare_classic_mifare_(std::vector<uint8_t> &uid) {
std::vector<uint8_t> blank_buffer(
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
std::vector<uint8_t> trailer_buffer(
@@ -140,7 +141,7 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
return !error;
}
bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) {
bool PN532::format_mifare_classic_ndef_(std::vector<uint8_t> &uid) {
std::vector<uint8_t> empty_ndef_message(
{0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
std::vector<uint8_t> blank_block(
@@ -215,7 +216,7 @@ bool PN532::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t>
return true;
}
bool PN532::write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) {
bool PN532::write_mifare_classic_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message) {
auto encoded = message->encode();
uint32_t message_length = encoded.size();

View File

@@ -8,7 +8,7 @@ namespace pn532 {
static const char *const TAG = "pn532.mifare_ultralight";
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(nfc::NfcTagUid &uid) {
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(std::vector<uint8_t> &uid) {
std::vector<uint8_t> data;
// pages 3 to 6 contain various info we are interested in -- do one read to grab it all
if (!this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE,
@@ -114,7 +114,7 @@ bool PN532::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6
return false;
}
bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) {
bool PN532::write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message) {
uint32_t capacity = this->read_mifare_ultralight_capacity_();
auto encoded = message->encode();

View File

@@ -478,7 +478,7 @@ uint8_t PN7150::read_endpoint_data_(nfc::NfcTag &tag) {
return nfc::STATUS_FAILED;
}
uint8_t PN7150::clean_endpoint_(nfc::NfcTagUid &uid) {
uint8_t PN7150::clean_endpoint_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -494,7 +494,7 @@ uint8_t PN7150::clean_endpoint_(nfc::NfcTagUid &uid) {
return nfc::STATUS_FAILED;
}
uint8_t PN7150::format_endpoint_(nfc::NfcTagUid &uid) {
uint8_t PN7150::format_endpoint_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -510,7 +510,7 @@ uint8_t PN7150::format_endpoint_(nfc::NfcTagUid &uid) {
return nfc::STATUS_FAILED;
}
uint8_t PN7150::write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t PN7150::write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -534,7 +534,7 @@ std::unique_ptr<nfc::NfcTag> PN7150::build_tag_(const uint8_t mode_tech, const s
ESP_LOGE(TAG, "UID length cannot be zero");
return nullptr;
}
nfc::NfcTagUid uid(data.begin() + 3, data.begin() + 3 + uid_length);
std::vector<uint8_t> uid(data.begin() + 3, data.begin() + 3 + uid_length);
const auto *tag_type_str =
nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2;
return make_unique<nfc::NfcTag>(uid, tag_type_str);
@@ -543,7 +543,7 @@ std::unique_ptr<nfc::NfcTag> PN7150::build_tag_(const uint8_t mode_tech, const s
return nullptr;
}
optional<size_t> PN7150::find_tag_uid_(const nfc::NfcTagUid &uid) {
optional<size_t> PN7150::find_tag_uid_(const std::vector<uint8_t> &uid) {
if (!this->discovered_endpoint_.empty()) {
for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) {
auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid();

View File

@@ -203,12 +203,12 @@ class PN7150 : public nfc::Nfcc, public Component {
void select_endpoint_();
uint8_t read_endpoint_data_(nfc::NfcTag &tag);
uint8_t clean_endpoint_(nfc::NfcTagUid &uid);
uint8_t format_endpoint_(nfc::NfcTagUid &uid);
uint8_t write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_endpoint_(std::vector<uint8_t> &uid);
uint8_t format_endpoint_(std::vector<uint8_t> &uid);
uint8_t write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message);
std::unique_ptr<nfc::NfcTag> build_tag_(uint8_t mode_tech, const std::vector<uint8_t> &data);
optional<size_t> find_tag_uid_(const nfc::NfcTagUid &uid);
optional<size_t> find_tag_uid_(const std::vector<uint8_t> &uid);
void purge_old_tags_();
void erase_tag_(uint8_t tag_index);
@@ -251,7 +251,7 @@ class PN7150 : public nfc::Nfcc, public Component {
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index);
uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_mifare_ultralight_();
enum NfcTask : uint8_t {

View File

@@ -115,7 +115,8 @@ uint8_t PN7150::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_
return nfc::STATUS_FAILED;
}
uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t PN7150::write_mifare_ultralight_tag_(std::vector<uint8_t> &uid,
const std::shared_ptr<nfc::NdefMessage> &message) {
uint32_t capacity = this->read_mifare_ultralight_capacity_();
auto encoded = message->encode();

View File

@@ -506,7 +506,7 @@ uint8_t PN7160::read_endpoint_data_(nfc::NfcTag &tag) {
return nfc::STATUS_FAILED;
}
uint8_t PN7160::clean_endpoint_(nfc::NfcTagUid &uid) {
uint8_t PN7160::clean_endpoint_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -522,7 +522,7 @@ uint8_t PN7160::clean_endpoint_(nfc::NfcTagUid &uid) {
return nfc::STATUS_FAILED;
}
uint8_t PN7160::format_endpoint_(nfc::NfcTagUid &uid) {
uint8_t PN7160::format_endpoint_(std::vector<uint8_t> &uid) {
uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -538,7 +538,7 @@ uint8_t PN7160::format_endpoint_(nfc::NfcTagUid &uid) {
return nfc::STATUS_FAILED;
}
uint8_t PN7160::write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t PN7160::write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -562,7 +562,7 @@ std::unique_ptr<nfc::NfcTag> PN7160::build_tag_(const uint8_t mode_tech, const s
ESP_LOGE(TAG, "UID length cannot be zero");
return nullptr;
}
nfc::NfcTagUid uid(data.begin() + 3, data.begin() + 3 + uid_length);
std::vector<uint8_t> uid(data.begin() + 3, data.begin() + 3 + uid_length);
const auto *tag_type_str =
nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2;
return make_unique<nfc::NfcTag>(uid, tag_type_str);
@@ -571,7 +571,7 @@ std::unique_ptr<nfc::NfcTag> PN7160::build_tag_(const uint8_t mode_tech, const s
return nullptr;
}
optional<size_t> PN7160::find_tag_uid_(const nfc::NfcTagUid &uid) {
optional<size_t> PN7160::find_tag_uid_(const std::vector<uint8_t> &uid) {
if (!this->discovered_endpoint_.empty()) {
for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) {
auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid();

View File

@@ -220,12 +220,12 @@ class PN7160 : public nfc::Nfcc, public Component {
void select_endpoint_();
uint8_t read_endpoint_data_(nfc::NfcTag &tag);
uint8_t clean_endpoint_(nfc::NfcTagUid &uid);
uint8_t format_endpoint_(nfc::NfcTagUid &uid);
uint8_t write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_endpoint_(std::vector<uint8_t> &uid);
uint8_t format_endpoint_(std::vector<uint8_t> &uid);
uint8_t write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message);
std::unique_ptr<nfc::NfcTag> build_tag_(uint8_t mode_tech, const std::vector<uint8_t> &data);
optional<size_t> find_tag_uid_(const nfc::NfcTagUid &uid);
optional<size_t> find_tag_uid_(const std::vector<uint8_t> &uid);
void purge_old_tags_();
void erase_tag_(uint8_t tag_index);
@@ -268,7 +268,7 @@ class PN7160 : public nfc::Nfcc, public Component {
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index);
uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_mifare_ultralight_();
enum NfcTask : uint8_t {

View File

@@ -115,7 +115,8 @@ uint8_t PN7160::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_
return nfc::STATUS_FAILED;
}
uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t PN7160::write_mifare_ultralight_tag_(std::vector<uint8_t> &uid,
const std::shared_ptr<nfc::NdefMessage> &message) {
uint32_t capacity = this->read_mifare_ultralight_capacity_();
auto encoded = message->encode();

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;
}
}
this->type_ = Sen5xType::UNKNOWN;
ESP_LOGE(TAG, "Unknown product name: %.32s", product_name);
this->error_code_ = PRODUCT_NAME_FAILED;
this->mark_failed();
return;
}
ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str());
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,12 +160,8 @@ 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);
// 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_)) {
@@ -262,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());
}

View File

@@ -24,6 +24,8 @@ enum RhtAccelerationMode : uint16_t {
HIGH_ACCELERATION = 2,
};
enum class Sen5xType : uint8_t { SEN50, SEN54, SEN55, UNKNOWN };
struct GasTuning {
uint16_t index_offset;
uint16_t learning_time_offset_hours;
@@ -49,8 +51,6 @@ 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; }
@@ -102,11 +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);
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_;
@@ -127,7 +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_;
};
} // namespace sen5x

View File

@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <cstdint>
namespace esphome {
@@ -21,7 +22,7 @@ enum SmlMessageType : uint16_t { SML_PUBLIC_OPEN_RES = 0x0101, SML_GET_LIST_RES
const uint16_t START_MASK = 0x55aa; // 0x1b 1b 1b 1b 01 01 01 01
const uint16_t END_MASK = 0x0157; // 0x1b 1b 1b 1b 1a
const std::vector<uint8_t> START_SEQ = {0x1b, 0x1b, 0x1b, 0x1b, 0x01, 0x01, 0x01, 0x01};
constexpr std::array<uint8_t, 8> START_SEQ = {0x1b, 0x1b, 0x1b, 0x1b, 0x01, 0x01, 0x01, 0x01};
} // namespace sml
} // namespace esphome

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

@@ -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();

View File

@@ -13,7 +13,7 @@ enum TemplateWaterHeaterRestoreMode {
WATER_HEATER_RESTORE_AND_CALL,
};
class TemplateWaterHeater : public water_heater::WaterHeater {
class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
public:
TemplateWaterHeater();

View File

@@ -35,9 +35,6 @@ void TM1638Component::setup() {
this->set_intensity(intensity_);
this->reset_(); // all LEDs off
for (uint8_t i = 0; i < 8; i++) // zero fill print buffer
this->buffer_[i] = 0;
}
void TM1638Component::dump_config() {

View File

@@ -70,7 +70,7 @@ class TM1638Component : public PollingComponent {
GPIOPin *clk_pin_;
GPIOPin *stb_pin_;
GPIOPin *dio_pin_;
uint8_t *buffer_ = new uint8_t[8];
uint8_t buffer_[8]{};
tm1638_writer_t writer_{};
std::vector<KeyListener *> listeners_{};
};

View File

@@ -1,11 +1,23 @@
#include "uln2003.h"
#include "esphome/core/log.h"
namespace esphome {
namespace uln2003 {
namespace esphome::uln2003 {
static const char *const TAG = "uln2003.stepper";
static const LogString *step_mode_to_log_string(ULN2003StepMode mode) {
switch (mode) {
case ULN2003_STEP_MODE_FULL_STEP:
return LOG_STR("FULL STEP");
case ULN2003_STEP_MODE_HALF_STEP:
return LOG_STR("HALF STEP");
case ULN2003_STEP_MODE_WAVE_DRIVE:
return LOG_STR("WAVE DRIVE");
default:
return LOG_STR("UNKNOWN");
}
}
void ULN2003::setup() {
this->pin_a_->setup();
this->pin_b_->setup();
@@ -42,22 +54,7 @@ void ULN2003::dump_config() {
LOG_PIN(" Pin B: ", this->pin_b_);
LOG_PIN(" Pin C: ", this->pin_c_);
LOG_PIN(" Pin D: ", this->pin_d_);
const char *step_mode_s;
switch (this->step_mode_) {
case ULN2003_STEP_MODE_FULL_STEP:
step_mode_s = "FULL STEP";
break;
case ULN2003_STEP_MODE_HALF_STEP:
step_mode_s = "HALF STEP";
break;
case ULN2003_STEP_MODE_WAVE_DRIVE:
step_mode_s = "WAVE DRIVE";
break;
default:
step_mode_s = "UNKNOWN";
break;
}
ESP_LOGCONFIG(TAG, " Step Mode: %s", step_mode_s);
ESP_LOGCONFIG(TAG, " Step Mode: %s", LOG_STR_ARG(step_mode_to_log_string(this->step_mode_)));
}
void ULN2003::write_step_(int32_t step) {
int32_t n = this->step_mode_ == ULN2003_STEP_MODE_HALF_STEP ? 8 : 4;
@@ -90,5 +87,4 @@ void ULN2003::write_step_(int32_t step) {
this->pin_d_->digital_write((res >> 3) & 1);
}
} // namespace uln2003
} // namespace esphome
} // namespace esphome::uln2003

View File

@@ -4,8 +4,7 @@
#include "esphome/core/hal.h"
#include "esphome/components/stepper/stepper.h"
namespace esphome {
namespace uln2003 {
namespace esphome::uln2003 {
enum ULN2003StepMode {
ULN2003_STEP_MODE_FULL_STEP,
@@ -40,5 +39,4 @@ class ULN2003 : public stepper::Stepper, public Component {
int32_t current_uln_pos_{0};
};
} // namespace uln2003
} // namespace esphome
} // namespace esphome::uln2003

View File

@@ -2,6 +2,8 @@
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <strings.h>
namespace esphome {
@@ -38,13 +40,13 @@ Valve::Valve() : position{VALVE_OPEN} {}
ValveCall::ValveCall(Valve *parent) : parent_(parent) {}
ValveCall &ValveCall::set_command(const char *command) {
if (strcasecmp(command, "OPEN") == 0) {
if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("OPEN")) == 0) {
this->set_command_open();
} else if (strcasecmp(command, "CLOSE") == 0) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLOSE")) == 0) {
this->set_command_close();
} else if (strcasecmp(command, "STOP") == 0) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) {
this->set_command_stop();
} else if (strcasecmp(command, "TOGGLE") == 0) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) {
this->set_command_toggle();
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command);

View File

@@ -18,7 +18,7 @@ CODEOWNERS = ["@dhoeben"]
IS_PLATFORM_COMPONENT = True
water_heater_ns = cg.esphome_ns.namespace("water_heater")
WaterHeater = water_heater_ns.class_("WaterHeater", cg.EntityBase, cg.Component)
WaterHeater = water_heater_ns.class_("WaterHeater", cg.EntityBase)
WaterHeaterCall = water_heater_ns.class_("WaterHeaterCall")
WaterHeaterTraits = water_heater_ns.class_("WaterHeaterTraits")
@@ -46,7 +46,7 @@ _WATER_HEATER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
}
),
}
).extend(cv.COMPONENT_SCHEMA)
)
_WATER_HEATER_SCHEMA.add_extra(entity_duplicate_validator("water_heater"))
@@ -91,8 +91,6 @@ async def register_water_heater(var: cg.Pvariable, config: ConfigType) -> cg.Pva
cg.add_define("USE_WATER_HEATER")
await cg.register_component(var, config)
cg.add(cg.App.register_water_heater(var))
CORE.register_platform_component("water_heater", var)

View File

@@ -146,10 +146,6 @@ void WaterHeaterCall::validate_() {
}
}
void WaterHeater::setup() {
this->pref_ = global_preferences->make_preference<SavedWaterHeaterState>(this->get_preference_hash());
}
void WaterHeater::publish_state() {
auto traits = this->get_traits();
ESP_LOGD(TAG,
@@ -188,7 +184,8 @@ void WaterHeater::publish_state() {
this->pref_.save(&saved);
}
optional<WaterHeaterCall> WaterHeater::restore_state() {
optional<WaterHeaterCall> WaterHeater::restore_state_() {
this->pref_ = global_preferences->make_preference<SavedWaterHeaterState>(this->get_preference_hash());
SavedWaterHeaterState recovered{};
if (!this->pref_.load(&recovered))
return {};

View File

@@ -177,7 +177,7 @@ class WaterHeaterTraits {
WaterHeaterModeMask supported_modes_;
};
class WaterHeater : public EntityBase, public Component {
class WaterHeater : public EntityBase {
public:
WaterHeaterMode get_mode() const { return this->mode_; }
float get_current_temperature() const { return this->current_temperature_; }
@@ -204,16 +204,15 @@ class WaterHeater : public EntityBase, public Component {
#endif
virtual void control(const WaterHeaterCall &call) = 0;
void setup() override;
optional<WaterHeaterCall> restore_state();
protected:
virtual WaterHeaterTraits traits() = 0;
/// Log the traits of this water heater for dump_config().
void dump_traits_(const char *tag);
/// Restore the state of the water heater, call this from your setup() method.
optional<WaterHeaterCall> restore_state_();
/// Set the mode of the water heater. Should only be called from control().
void set_mode_(WaterHeaterMode mode) { this->mode_ = mode; }
/// Set the target temperature of the water heater. Should only be called from control().

View File

@@ -698,6 +698,10 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
if (!this->wifi_mode_(true, {}))
return false;
// Reset scan_done_ before starting new scan to prevent stale flag from previous scan
// (e.g., roaming scan completed just before unexpected disconnect)
this->scan_done_ = false;
struct scan_config config {};
memset(&config, 0, sizeof(config));
config.ssid = nullptr;

View File

@@ -14,6 +14,7 @@
#include <algorithm>
#include <cinttypes>
#include <memory>
#include <utility>
#ifdef USE_WIFI_WPA2_EAP
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
@@ -828,16 +829,29 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
uint16_t number = it.number;
scan_result_.init(number);
// Process one record at a time to avoid large buffer allocation
wifi_ap_record_t record;
#ifdef USE_ESP32_HOSTED
// getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor
// Presumably an upstream bug, work-around by getting all records at once
auto records = std::make_unique<wifi_ap_record_t[]>(number);
err = esp_wifi_scan_get_ap_records(&number, records.get());
if (err != ESP_OK) {
esp_wifi_clear_ap_list();
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err));
return;
}
for (uint16_t i = 0; i < number; i++) {
wifi_ap_record_t &record = records[i];
#else
// Process one record at a time to avoid large buffer allocation
for (uint16_t i = 0; i < number; i++) {
wifi_ap_record_t record;
err = esp_wifi_scan_get_ap_record(&record);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_record failed: %s", esp_err_to_name(err));
esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved
break;
}
#endif // USE_ESP32_HOSTED
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
std::string ssid(reinterpret_cast<const char *>(record.ssid));

View File

@@ -649,6 +649,10 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
if (!this->wifi_mode_(true, {}))
return false;
// Reset scan_done_ before starting new scan to prevent stale flag from previous scan
// (e.g., roaming scan completed just before unexpected disconnect)
this->scan_done_ = false;
// need to use WiFi because of WiFiScanClass allocations :(
int16_t err = WiFi.scanNetworks(true, true, passive, 200);
if (err != WIFI_SCAN_RUNNING) {

View File

@@ -42,6 +42,7 @@
#define USE_DEVICES
#define USE_DISPLAY
#define USE_ENTITY_ICON
#define USE_ESP32_HOSTED
#define USE_ESP32_IMPROV_STATE_CALLBACK
#define USE_EVENT
#define USE_FAN

View File

@@ -3,6 +3,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
#include <strings.h>
@@ -451,15 +452,15 @@ std::string format_bin(const uint8_t *data, size_t length) {
}
ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
if (on == nullptr && strcasecmp(str, "on") == 0)
if (on == nullptr && ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("on")) == 0)
return PARSE_ON;
if (on != nullptr && strcasecmp(str, on) == 0)
return PARSE_ON;
if (off == nullptr && strcasecmp(str, "off") == 0)
if (off == nullptr && ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("off")) == 0)
return PARSE_OFF;
if (off != nullptr && strcasecmp(str, off) == 0)
return PARSE_OFF;
if (strcasecmp(str, "toggle") == 0)
if (ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("toggle")) == 0)
return PARSE_TOGGLE;
return PARSE_NONE;

View File

@@ -148,25 +148,6 @@ template<typename T, size_t N> class StaticVector {
size_t count_{0};
public:
// Default constructor
StaticVector() = default;
// Iterator range constructor
template<typename InputIt> StaticVector(InputIt first, InputIt last) {
while (first != last && count_ < N) {
data_[count_++] = *first++;
}
}
// Initializer list constructor
StaticVector(std::initializer_list<T> init) {
for (const auto &val : init) {
if (count_ >= N)
break;
data_[count_++] = val;
}
}
// Minimal vector-compatible interface - only what we actually use
void push_back(const T &value) {
if (count_ < N) {
@@ -174,17 +155,6 @@ template<typename T, size_t N> class StaticVector {
}
}
// Clear all elements
void clear() { count_ = 0; }
// Assign from iterator range
template<typename InputIt> void assign(InputIt first, InputIt last) {
count_ = 0;
while (first != last && count_ < N) {
data_[count_++] = *first++;
}
}
// Return reference to next element and increment count (with bounds checking)
T &emplace_next() {
if (count_ >= N) {
@@ -216,10 +186,6 @@ template<typename T, size_t N> class StaticVector {
reverse_iterator rend() { return reverse_iterator(begin()); }
const_reverse_iterator rbegin() const { return const_reverse_iterator(end()); }
const_reverse_iterator rend() const { return const_reverse_iterator(begin()); }
// Conversion to std::span for compatibility with span-based APIs
operator std::span<T>() { return std::span<T>(data_.data(), count_); }
operator std::span<const T>() const { return std::span<const T>(data_.data(), count_); }
};
/// Fixed-capacity vector - allocates once at runtime, never reallocates

View File

@@ -12,6 +12,10 @@
#define ESPHOME_strncpy_P strncpy_P
#define ESPHOME_strncat_P strncat_P
#define ESPHOME_snprintf_P snprintf_P
#define ESPHOME_strcmp_P strcmp_P
#define ESPHOME_strcasecmp_P strcasecmp_P
#define ESPHOME_strncmp_P strncmp_P
#define ESPHOME_strncasecmp_P strncasecmp_P
// Type for pointers to PROGMEM strings (for use with ESPHOME_F return values)
using ProgmemStr = const __FlashStringHelper *;
#else
@@ -21,6 +25,10 @@ using ProgmemStr = const __FlashStringHelper *;
#define ESPHOME_strncpy_P strncpy
#define ESPHOME_strncat_P strncat
#define ESPHOME_snprintf_P snprintf
#define ESPHOME_strcmp_P strcmp
#define ESPHOME_strcasecmp_P strcasecmp
#define ESPHOME_strncmp_P strncmp
#define ESPHOME_strncasecmp_P strncasecmp
// Type for pointers to strings (no PROGMEM on non-ESP8266 platforms)
using ProgmemStr = const char *;
#endif

View File

@@ -154,6 +154,12 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None
"""
if not expect:
return
if not data:
raise OTAError(
"Error: Device closed connection without responding. "
"This may indicate the device ran out of memory, "
"a network issue, or the connection was interrupted."
)
dat = data[0]
if dat == RESPONSE_ERROR_MAGIC:
raise OTAError("Error: Invalid magic byte")

View File

@@ -212,7 +212,7 @@ build_unflags =
; This are common settings for the LibreTiny (all variants) using Arduino.
[common:libretiny-arduino]
extends = common:arduino
platform = libretiny@1.9.2
platform = https://github.com/libretiny-eu/libretiny.git#v1.11.0
framework = arduino
lib_compat_mode = soft
lib_deps =

View File

@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools==80.10.1", "wheel>=0.43,<0.47"]
requires = ["setuptools==80.10.2", "wheel>=0.43,<0.47"]
build-backend = "setuptools.build_meta"
[project]

View File

@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0
click==8.1.7
esphome-dashboard==20260110.0
aioesphomeapi==43.13.0
aioesphomeapi==43.14.0
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.19.1 # dashboard_import

View File

@@ -0,0 +1,7 @@
substitutions:
verify_ssl: "true"
http_request:
ca_certificate_path: $component_dir/test_ca.pem
<<: !include common.yaml

View File

@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBkTCB+wIJAKHBfpegPjMCMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBnVu
dXNlZDAeFw0yNDAxMDEwMDAwMDBaFw0yNTAxMDEwMDAwMDBaMBExDzANBgNVBAMM
BnVudXNlZDBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC5mMUB1hOgLmlnXtsvcGMP
XkhAqZaR0dDPW5OS8VEopWLJCX9Y0cvNCqiDI8cnP8pP8XJGU1hGLvA5PJzWnWZz
AgMBAAGjUzBRMB0GA1UdDgQWBBR5oQ9KqFeZOdBuAJrXxEP0dqzPtTAfBgNVHSME
GDAWgBR5oQ9KqFeZOdBuAJrXxEP0dqzPtTAPBgNVHRMBAf8EBTADAQH/MA0GCSqG
SIb3DQEBCwUAA0EAKqZFf6+f8FPDbKyPCpssquojgn7fEXqr/I/yz0R5CowGdMms
H3WH3aKP4lLSHdPTBtfIoJi3gEIZjFxp3S1TWw==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,57 @@
sy6970:
id: sy6970_component
i2c_id: i2c_bus
address: 0x6A
enable_status_led: true
input_current_limit: 1000
charge_voltage: 4200
charge_current: 500
precharge_current: 128
charge_enabled: true
enable_adc: true
update_interval: 5s
sensor:
- platform: sy6970
sy6970_id: sy6970_component
vbus_voltage:
name: "VBUS Voltage"
id: vbus_voltage_sensor
battery_voltage:
name: "Battery Voltage"
id: battery_voltage_sensor
system_voltage:
name: "System Voltage"
id: system_voltage_sensor
charge_current:
name: "Charge Current"
id: charge_current_sensor
precharge_current:
name: "Precharge Current"
id: precharge_current_sensor
binary_sensor:
- platform: sy6970
sy6970_id: sy6970_component
vbus_connected:
name: "VBUS Connected"
id: vbus_connected_binary
charging:
name: "Charging"
id: charging_binary
charge_done:
name: "Charge Done"
id: charge_done_binary
text_sensor:
- platform: sy6970
sy6970_id: sy6970_component
bus_status:
name: "Bus Status"
id: bus_status_text
charge_status:
name: "Charge Status"
id: charge_status_text
ntc_status:
name: "NTC Status"
id: ntc_status_text

View File

@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml

View File

@@ -64,3 +64,16 @@ text_sensor:
- suffix -> SUFFIX
- map:
- PREFIX text SUFFIX -> mapped
- platform: template
name: "Test Lambda Filter"
id: test_lambda_filter
filters:
- lambda: |-
return {"[" + x + "]"};
- to_upper
- lambda: |-
if (x.length() > 10) {
return {x.substr(0, 10) + "..."};
}
return {x};

View File

@@ -56,6 +56,36 @@ text_sensor:
- prepend: "["
- append: "]"
- platform: template
name: "To Lower Sensor"
id: to_lower_sensor
filters:
- to_lower
- platform: template
name: "Lambda Sensor"
id: lambda_sensor
filters:
- lambda: |-
return {"[" + x + "]"};
- platform: template
name: "Lambda Raw State Sensor"
id: lambda_raw_state_sensor
filters:
- lambda: |-
return {x + " MODIFIED"};
- platform: template
name: "Lambda Skip Sensor"
id: lambda_skip_sensor
filters:
- lambda: |-
if (x == "skip") {
return {};
}
return {x + " passed"};
# Button to publish values and log raw_state vs state
button:
- platform: template
@@ -179,3 +209,73 @@ button:
format: "CHAINED: state='%s'"
args:
- id(chained_sensor).state.c_str()
- platform: template
name: "Test To Lower Button"
id: test_to_lower_button
on_press:
- text_sensor.template.publish:
id: to_lower_sensor
state: "HELLO WORLD"
- delay: 50ms
- logger.log:
format: "TO_LOWER: state='%s'"
args:
- id(to_lower_sensor).state.c_str()
- platform: template
name: "Test Lambda Button"
id: test_lambda_button
on_press:
- text_sensor.template.publish:
id: lambda_sensor
state: "test"
- delay: 50ms
- logger.log:
format: "LAMBDA: state='%s'"
args:
- id(lambda_sensor).state.c_str()
- platform: template
name: "Test Lambda Pass Button"
id: test_lambda_pass_button
on_press:
- text_sensor.template.publish:
id: lambda_skip_sensor
state: "value"
- delay: 50ms
- logger.log:
format: "LAMBDA_PASS: state='%s'"
args:
- id(lambda_skip_sensor).state.c_str()
- platform: template
name: "Test Lambda Skip Button"
id: test_lambda_skip_button
on_press:
- text_sensor.template.publish:
id: lambda_skip_sensor
state: "skip"
- delay: 50ms
# When lambda returns {}, the value should NOT be published
# so state should remain from previous publish (or empty if first)
- logger.log:
format: "LAMBDA_SKIP: state='%s'"
args:
- id(lambda_skip_sensor).state.c_str()
- platform: template
name: "Test Lambda Raw State Button"
id: test_lambda_raw_state_button
on_press:
- text_sensor.template.publish:
id: lambda_raw_state_sensor
state: "original"
- delay: 50ms
# Verify raw_state is preserved (not mutated) after lambda filter
# state should be "original MODIFIED", raw_state should be "original"
- logger.log:
format: "LAMBDA_RAW_STATE: state='%s' raw_state='%s'"
args:
- id(lambda_raw_state_sensor).state.c_str()
- id(lambda_raw_state_sensor).get_raw_state().c_str()

View File

@@ -42,6 +42,11 @@ async def test_text_sensor_raw_state(
map_off_future: asyncio.Future[str] = loop.create_future()
map_unknown_future: asyncio.Future[str] = loop.create_future()
chained_future: asyncio.Future[str] = loop.create_future()
to_lower_future: asyncio.Future[str] = loop.create_future()
lambda_future: asyncio.Future[str] = loop.create_future()
lambda_pass_future: asyncio.Future[str] = loop.create_future()
lambda_skip_future: asyncio.Future[str] = loop.create_future()
lambda_raw_state_future: asyncio.Future[tuple[str, str]] = loop.create_future()
# Patterns to match log output
# NO_FILTER: state='hello world' raw_state='hello world'
@@ -58,6 +63,13 @@ async def test_text_sensor_raw_state(
map_off_pattern = re.compile(r"MAP_OFF: state='([^']*)'")
map_unknown_pattern = re.compile(r"MAP_UNKNOWN: state='([^']*)'")
chained_pattern = re.compile(r"CHAINED: state='([^']*)'")
to_lower_pattern = re.compile(r"TO_LOWER: state='([^']*)'")
lambda_pattern = re.compile(r"LAMBDA: state='([^']*)'")
lambda_pass_pattern = re.compile(r"LAMBDA_PASS: state='([^']*)'")
lambda_skip_pattern = re.compile(r"LAMBDA_SKIP: state='([^']*)'")
lambda_raw_state_pattern = re.compile(
r"LAMBDA_RAW_STATE: state='([^']*)' raw_state='([^']*)'"
)
def check_output(line: str) -> None:
"""Check log output for expected messages."""
@@ -92,6 +104,27 @@ async def test_text_sensor_raw_state(
if not chained_future.done() and (match := chained_pattern.search(line)):
chained_future.set_result(match.group(1))
if not to_lower_future.done() and (match := to_lower_pattern.search(line)):
to_lower_future.set_result(match.group(1))
if not lambda_future.done() and (match := lambda_pattern.search(line)):
lambda_future.set_result(match.group(1))
if not lambda_pass_future.done() and (
match := lambda_pass_pattern.search(line)
):
lambda_pass_future.set_result(match.group(1))
if not lambda_skip_future.done() and (
match := lambda_skip_pattern.search(line)
):
lambda_skip_future.set_result(match.group(1))
if not lambda_raw_state_future.done() and (
match := lambda_raw_state_pattern.search(line)
):
lambda_raw_state_future.set_result((match.group(1), match.group(2)))
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
@@ -272,3 +305,111 @@ async def test_text_sensor_raw_state(
pytest.fail("Timeout waiting for CHAINED log message")
assert state == "[value]", f"Chained failed: expected '[value]', got '{state}'"
# Test 10: to_lower filter
# "HELLO WORLD" -> "hello world"
to_lower_button = next(
(e for e in entities if "test_to_lower_button" in e.object_id.lower()),
None,
)
assert to_lower_button is not None, "Test To Lower Button not found"
client.button_command(to_lower_button.key)
try:
state = await asyncio.wait_for(to_lower_future, timeout=5.0)
except TimeoutError:
pytest.fail("Timeout waiting for TO_LOWER log message")
assert state == "hello world", (
f"to_lower failed: expected 'hello world', got '{state}'"
)
# Test 11: Lambda filter
# "test" -> "[test]"
lambda_button = next(
(e for e in entities if "test_lambda_button" in e.object_id.lower()),
None,
)
assert lambda_button is not None, "Test Lambda Button not found"
client.button_command(lambda_button.key)
try:
state = await asyncio.wait_for(lambda_future, timeout=5.0)
except TimeoutError:
pytest.fail("Timeout waiting for LAMBDA log message")
assert state == "[test]", f"Lambda failed: expected '[test]', got '{state}'"
# Test 12: Lambda filter - value passes through
# "value" -> "value passed"
lambda_pass_button = next(
(e for e in entities if "test_lambda_pass_button" in e.object_id.lower()),
None,
)
assert lambda_pass_button is not None, "Test Lambda Pass Button not found"
client.button_command(lambda_pass_button.key)
try:
state = await asyncio.wait_for(lambda_pass_future, timeout=5.0)
except TimeoutError:
pytest.fail("Timeout waiting for LAMBDA_PASS log message")
assert state == "value passed", (
f"Lambda pass failed: expected 'value passed', got '{state}'"
)
# Test 13: Lambda filter - skip publishing (return {})
# "skip" -> no publish, state remains "value passed" from previous test
lambda_skip_button = next(
(e for e in entities if "test_lambda_skip_button" in e.object_id.lower()),
None,
)
assert lambda_skip_button is not None, "Test Lambda Skip Button not found"
client.button_command(lambda_skip_button.key)
try:
state = await asyncio.wait_for(lambda_skip_future, timeout=5.0)
except TimeoutError:
pytest.fail("Timeout waiting for LAMBDA_SKIP log message")
# When lambda returns {}, value should NOT be published
# State remains from previous successful publish ("value passed")
assert state == "value passed", (
f"Lambda skip failed: expected 'value passed' (unchanged), got '{state}'"
)
# Test 14: Lambda filter - verify raw_state is preserved (not mutated)
# This is critical to verify the in-place mutation optimization is safe
# "original" -> state="original MODIFIED", raw_state="original"
lambda_raw_state_button = next(
(
e
for e in entities
if "test_lambda_raw_state_button" in e.object_id.lower()
),
None,
)
assert lambda_raw_state_button is not None, (
"Test Lambda Raw State Button not found"
)
client.button_command(lambda_raw_state_button.key)
try:
state, raw_state = await asyncio.wait_for(
lambda_raw_state_future, timeout=5.0
)
except TimeoutError:
pytest.fail("Timeout waiting for LAMBDA_RAW_STATE log message")
assert state == "original MODIFIED", (
f"Lambda raw_state test failed: expected state='original MODIFIED', "
f"got '{state}'"
)
assert raw_state == "original", (
f"Lambda raw_state test failed: raw_state was mutated! "
f"Expected 'original', got '{raw_state}'"
)
assert state != raw_state, (
f"Lambda filter should modify state but preserve raw_state. "
f"state='{state}', raw_state='{raw_state}'"
)

View File

@@ -192,6 +192,20 @@ def test_check_error_unexpected_response() -> None:
espota2.check_error([0x7F], [espota2.RESPONSE_OK, espota2.RESPONSE_AUTH_OK])
def test_check_error_empty_data() -> None:
"""Test check_error raises error when device closes connection without responding."""
with pytest.raises(
espota2.OTAError, match="Device closed connection without responding"
):
espota2.check_error([], [espota2.RESPONSE_OK])
# Also test with empty bytes
with pytest.raises(
espota2.OTAError, match="Device closed connection without responding"
):
espota2.check_error(b"", [espota2.RESPONSE_OK])
def test_send_check_with_various_data_types(mock_socket: Mock) -> None:
"""Test send_check handles different data types."""