Merge branch 'mqtt_enum_flash' into integration

This commit is contained in:
J. Nick Koston
2026-01-26 20:34:49 -10:00
11 changed files with 298 additions and 11 deletions
+4
View File
@@ -1 +1,5 @@
<<<<<<< HEAD
a172e2f65981e98354cc6b5ecf69bdb055dd13602226042ab2c7acd037a2bf41
=======
d565b0589e35e692b5f2fc0c14723a99595b4828a3a3ef96c442e86a23176c00
>>>>>>> mqtt_enum_flash
+9 -2
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",
@@ -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_;
@@ -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);
+2 -1
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
-3
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() {
@@ -0,0 +1,7 @@
substitutions:
verify_ssl: "true"
http_request:
ca_certificate_path: $component_dir/test_ca.pem
<<: !include common.yaml
+10
View File
@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBkTCB+wIJAKHBfpegPjMCMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBnVu
dXNlZDAeFw0yNDAxMDEwMDAwMDBaFw0yNTAxMDEwMDAwMDBaMBExDzANBgNVBAMM
BnVudXNlZDBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC5mMUB1hOgLmlnXtsvcGMP
XkhAqZaR0dDPW5OS8VEopWLJCX9Y0cvNCqiDI8cnP8pP8XJGU1hGLvA5PJzWnWZz
AgMBAAGjUzBRMB0GA1UdDgQWBBR5oQ9KqFeZOdBuAJrXxEP0dqzPtTAfBgNVHSME
GDAWgBR5oQ9KqFeZOdBuAJrXxEP0dqzPtTAPBgNVHRMBAf8EBTADAQH/MA0GCSqG
SIb3DQEBCwUAA0EAKqZFf6+f8FPDbKyPCpssquojgn7fEXqr/I/yz0R5CowGdMms
H3WH3aKP4lLSHdPTBtfIoJi3gEIZjFxp3S1TWw==
-----END CERTIFICATE-----
+13
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};
@@ -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()
@@ -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}'"
)