[text_sensor] Avoid duplicate string storage when no filters configured (#12205)
This commit is contained in:
@@ -25,7 +25,11 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text
|
||||
}
|
||||
|
||||
void TextSensor::publish_state(const std::string &state) {
|
||||
this->raw_state = state;
|
||||
// Only store raw_state_ separately when filters exist
|
||||
// When no filters, raw_state == state, so we avoid the duplicate storage
|
||||
if (this->filter_list_ != nullptr) {
|
||||
this->raw_state_ = state;
|
||||
}
|
||||
if (this->raw_callback_) {
|
||||
this->raw_callback_->call(state);
|
||||
}
|
||||
@@ -80,7 +84,11 @@ void TextSensor::add_on_raw_state_callback(std::function<void(std::string)> call
|
||||
}
|
||||
|
||||
std::string TextSensor::get_state() const { return this->state; }
|
||||
std::string TextSensor::get_raw_state() const { return this->raw_state; }
|
||||
std::string TextSensor::get_raw_state() const {
|
||||
// When no filters exist, raw_state == state, so return state to avoid
|
||||
// requiring separate storage
|
||||
return this->filter_list_ != nullptr ? this->raw_state_ : this->state;
|
||||
}
|
||||
void TextSensor::internal_send_state_to_frontend(const std::string &state) {
|
||||
this->state = state;
|
||||
this->set_has_state(true);
|
||||
|
||||
@@ -50,7 +50,6 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
|
||||
void add_on_raw_state_callback(std::function<void(std::string)> callback);
|
||||
|
||||
std::string state;
|
||||
std::string raw_state;
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
@@ -63,6 +62,10 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
|
||||
CallbackManager<void(std::string)> callback_; ///< Storage for filtered state callbacks.
|
||||
|
||||
Filter *filter_list_{nullptr}; ///< Store all active filters.
|
||||
|
||||
/// Raw state (before filters). Only populated when filters are configured.
|
||||
/// When no filters exist, get_raw_state() returns state directly.
|
||||
std::string raw_state_;
|
||||
};
|
||||
|
||||
} // namespace text_sensor
|
||||
|
||||
54
tests/integration/fixtures/text_sensor_raw_state.yaml
Normal file
54
tests/integration/fixtures/text_sensor_raw_state.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
esphome:
|
||||
name: test-text-sensor-raw-state
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
# Text sensor WITHOUT filters - get_raw_state() should return same as state
|
||||
text_sensor:
|
||||
- platform: template
|
||||
name: "No Filter Sensor"
|
||||
id: no_filter_sensor
|
||||
|
||||
# Text sensor WITH filter - get_raw_state() should return original value
|
||||
- platform: template
|
||||
name: "With Filter Sensor"
|
||||
id: with_filter_sensor
|
||||
filters:
|
||||
- to_upper
|
||||
|
||||
# Button to publish values and log raw_state vs state
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test No Filter Button"
|
||||
id: test_no_filter_button
|
||||
on_press:
|
||||
- text_sensor.template.publish:
|
||||
id: no_filter_sensor
|
||||
state: "hello world"
|
||||
- delay: 50ms
|
||||
# Log both state and get_raw_state() to verify they match
|
||||
- logger.log:
|
||||
format: "NO_FILTER: state='%s' raw_state='%s'"
|
||||
args:
|
||||
- id(no_filter_sensor).state.c_str()
|
||||
- id(no_filter_sensor).get_raw_state().c_str()
|
||||
|
||||
- platform: template
|
||||
name: "Test With Filter Button"
|
||||
id: test_with_filter_button
|
||||
on_press:
|
||||
- text_sensor.template.publish:
|
||||
id: with_filter_sensor
|
||||
state: "hello world"
|
||||
- delay: 50ms
|
||||
# Log both state and get_raw_state() to verify filter works
|
||||
# state should be "HELLO WORLD" (filtered), raw_state should be "hello world" (original)
|
||||
- logger.log:
|
||||
format: "WITH_FILTER: state='%s' raw_state='%s'"
|
||||
args:
|
||||
- id(with_filter_sensor).state.c_str()
|
||||
- id(with_filter_sensor).get_raw_state().c_str()
|
||||
114
tests/integration/test_text_sensor_raw_state.py
Normal file
114
tests/integration/test_text_sensor_raw_state.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Integration test for TextSensor get_raw_state() functionality.
|
||||
|
||||
This tests the optimization in PR #12205 where raw_state is only stored
|
||||
when filters are configured. When no filters exist, get_raw_state() should
|
||||
return state directly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_text_sensor_raw_state(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that get_raw_state() works correctly with and without filters.
|
||||
|
||||
Without filters: get_raw_state() should return the same value as state
|
||||
With filters: get_raw_state() should return the original (unfiltered) value
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Futures to track log messages
|
||||
no_filter_future: asyncio.Future[tuple[str, str]] = loop.create_future()
|
||||
with_filter_future: asyncio.Future[tuple[str, str]] = loop.create_future()
|
||||
|
||||
# Patterns to match log output
|
||||
# NO_FILTER: state='hello world' raw_state='hello world'
|
||||
no_filter_pattern = re.compile(r"NO_FILTER: state='([^']*)' raw_state='([^']*)'")
|
||||
# WITH_FILTER: state='HELLO WORLD' raw_state='hello world'
|
||||
with_filter_pattern = re.compile(
|
||||
r"WITH_FILTER: state='([^']*)' raw_state='([^']*)'"
|
||||
)
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for expected messages."""
|
||||
if not no_filter_future.done():
|
||||
match = no_filter_pattern.search(line)
|
||||
if match:
|
||||
no_filter_future.set_result((match.group(1), match.group(2)))
|
||||
|
||||
if not with_filter_future.done():
|
||||
match = with_filter_pattern.search(line)
|
||||
if match:
|
||||
with_filter_future.set_result((match.group(1), match.group(2)))
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify device info
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "test-text-sensor-raw-state"
|
||||
|
||||
# Get entities to find our buttons
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Find the test buttons
|
||||
no_filter_button = next(
|
||||
(e for e in entities if "test_no_filter_button" in e.object_id.lower()),
|
||||
None,
|
||||
)
|
||||
assert no_filter_button is not None, "Test No Filter Button not found"
|
||||
|
||||
with_filter_button = next(
|
||||
(e for e in entities if "test_with_filter_button" in e.object_id.lower()),
|
||||
None,
|
||||
)
|
||||
assert with_filter_button is not None, "Test With Filter Button not found"
|
||||
|
||||
# Test 1: Text sensor without filters
|
||||
# get_raw_state() should return the same as state
|
||||
client.button_command(no_filter_button.key)
|
||||
|
||||
try:
|
||||
state, raw_state = await asyncio.wait_for(no_filter_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for NO_FILTER log message")
|
||||
|
||||
assert state == "hello world", f"Expected state='hello world', got '{state}'"
|
||||
assert raw_state == "hello world", (
|
||||
f"Expected raw_state='hello world', got '{raw_state}'"
|
||||
)
|
||||
assert state == raw_state, (
|
||||
f"Without filters, state and raw_state should be equal. "
|
||||
f"state='{state}', raw_state='{raw_state}'"
|
||||
)
|
||||
|
||||
# Test 2: Text sensor with to_upper filter
|
||||
# state should be filtered (uppercase), raw_state should be original
|
||||
client.button_command(with_filter_button.key)
|
||||
|
||||
try:
|
||||
state, raw_state = await asyncio.wait_for(with_filter_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for WITH_FILTER log message")
|
||||
|
||||
assert state == "HELLO WORLD", f"Expected state='HELLO WORLD', got '{state}'"
|
||||
assert raw_state == "hello world", (
|
||||
f"Expected raw_state='hello world', got '{raw_state}'"
|
||||
)
|
||||
assert state != raw_state, (
|
||||
f"With filters, state and raw_state should differ. "
|
||||
f"state='{state}', raw_state='{raw_state}'"
|
||||
)
|
||||
Reference in New Issue
Block a user