[core] Keep string constants in apply conditions in flash on ESP8266 (#20107)

This commit is contained in:
J. Nick Koston
2026-10-12 15:48:05 -10:00
committed by GitHub
parent 5a39097b3a
commit 2001f60954
3 changed files with 57 additions and 11 deletions
+21 -6
View File
@@ -248,6 +248,23 @@ def flash_string(config: ConfigType, value: str) -> str:
return str(cg.progmem_string(value))
def flash_compare(config: ConfigType, value: str) -> str:
"""Default renderer for ``std::string`` constants in conditions; compares in flash on ESP8266."""
if CORE.is_esp8266:
return f"::esphome::ProgmemStringRef{{PSTR({cg.safe_exp(value)}), {len(value.encode('utf-8'))}}}"
return str(cg.safe_exp(value))
def _default_std_string(
members: list[tuple[str, Any, Any]], renderer: Callable[[ConfigType, str], str]
) -> list[tuple[str, Any, Any]]:
"""Give ``cg.std_string`` members without a renderer the given default."""
return [
(key, t, fn or (renderer if t is cg.std_string else None))
for key, t, fn in members
]
def literal_with_length(config: ConfigType, value: str) -> str:
"""Renderer for a ``(const char *, size_t)`` target: a plain literal plus its byte length.
@@ -491,10 +508,7 @@ def apply_action_builder(
statements_spec = [
(
c.target,
[
(key, t, fn or (flash_string if t is cg.std_string else None))
for key, t, fn in c.members
],
_default_std_string(c.members, flash_string),
)
for c in (f if isinstance(f, ApplyCall) else f.call() for f in fields)
]
@@ -563,11 +577,12 @@ def register_apply_condition(
``check`` is applied to the parent: ``"is_playing()"`` becomes ``parent->is_playing()``; an
``ApplyCall`` such as ``ApplyCall("state == {}", ((CONF_STATE, cg.bool_),))`` compares
against config values, all of which must be present. Write ``== false`` to negate.
String constants are plain literals, so compare a ``std::string`` or ``StringRef`` member.
A ``cg.std_string`` constant stays in flash on ESP8266 and supports only ``==`` or ``!=``
against a ``std::string`` or ``StringRef`` member; other string types are plain literals.
Generates one static predicate and an ``ApplyCondition`` templated on it.
"""
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
members = call.members
members = _default_std_string(call.members, flash_compare)
_check_key_in_schema(name, schema, id_key)
for conf_key, _, _ in members:
_check_key_in_schema(name, schema, conf_key)
+19
View File
@@ -188,6 +188,25 @@ inline bool operator==(const __FlashStringHelper *lhs, const StringRef &rhs) { r
inline bool operator!=(const StringRef &lhs, const __FlashStringHelper *rhs) { return !(lhs == rhs); }
inline bool operator!=(const __FlashStringHelper *lhs, const StringRef &rhs) { return !(rhs == lhs); }
/// Codegen use only: a flash string literal with its byte length, compared without a runtime strlen.
struct ProgmemStringRef {
PGM_P str;
size_t len;
};
inline bool operator==(const std::string &lhs, ProgmemStringRef rhs) {
return lhs.size() == rhs.len && (rhs.len == 0 || memcmp_P(lhs.data(), rhs.str, rhs.len) == 0);
}
inline bool operator!=(const std::string &lhs, ProgmemStringRef rhs) { return !(lhs == rhs); }
inline bool operator==(const StringRef &lhs, ProgmemStringRef rhs) {
// A StringRef may be {nullptr, 0}, and memcmp_P with a null pointer is undefined even for 0 bytes
return lhs.size() == rhs.len && (rhs.len == 0 || memcmp_P(lhs.c_str(), rhs.str, rhs.len) == 0);
}
inline bool operator!=(const StringRef &lhs, ProgmemStringRef rhs) { return !(lhs == rhs); }
#endif // USE_ESP8266
inline bool operator<(const StringRef &lhs, const StringRef &rhs) {
+17 -5
View File
@@ -1293,13 +1293,25 @@ async def test_apply_condition_compares_config_value(
@pytest.mark.asyncio
@pytest.mark.parametrize("platform", ["esp32", "esp8266"])
async def test_apply_condition_string_constant_is_a_plain_literal(
registries: tuple[Registry, Registry], mock_cg: MockCodegen, platform: str
@pytest.mark.parametrize(
("platform", "value", "expected"),
[
("esp32", "two", '"two"'),
("esp8266", "two", '::esphome::ProgmemStringRef{PSTR("two"), 3}'),
# The length is in bytes, not characters
("esp8266", "é", '::esphome::ProgmemStringRef{PSTR("\\303\\251"), 2}'),
],
)
async def test_apply_condition_string_constant(
registries: tuple[Registry, Registry],
mock_cg: MockCodegen,
platform: str,
value: str,
expected: str,
) -> None:
check = ApplyCall("state == {}", (("state", cg.std_string),))
await _run_apply_condition(registries, check, {"state": "two"}, platform=platform)
assert f'return ::{PARENT_OBJ}->state == "two";' in _apply_definition(mock_cg)
await _run_apply_condition(registries, check, {"state": value}, platform=platform)
assert f"return ::{PARENT_OBJ}->state == {expected};" in _apply_definition(mock_cg)
@pytest.mark.asyncio