mirror of
https://github.com/esphome/esphome.git
synced 2025-10-26 04:33:47 +00:00
text_sensor filters
This commit is contained in:
@@ -110,17 +110,28 @@ def validate_mapping(value):
|
|||||||
"substitute", SubstituteFilter, cv.ensure_list(validate_mapping)
|
"substitute", SubstituteFilter, cv.ensure_list(validate_mapping)
|
||||||
)
|
)
|
||||||
async def substitute_filter_to_code(config, filter_id):
|
async def substitute_filter_to_code(config, filter_id):
|
||||||
from_strings = [conf[CONF_FROM] for conf in config]
|
substitutions = [
|
||||||
to_strings = [conf[CONF_TO] for conf in config]
|
cg.StructInitializer(
|
||||||
return cg.new_Pvariable(filter_id, from_strings, to_strings)
|
cg.MockObj("Substitution", "esphome::text_sensor::"),
|
||||||
|
("from", conf[CONF_FROM]),
|
||||||
|
("to", conf[CONF_TO]),
|
||||||
|
)
|
||||||
|
for conf in config
|
||||||
|
]
|
||||||
|
return cg.new_Pvariable(filter_id, substitutions)
|
||||||
|
|
||||||
|
|
||||||
@FILTER_REGISTRY.register("map", MapFilter, cv.ensure_list(validate_mapping))
|
@FILTER_REGISTRY.register("map", MapFilter, cv.ensure_list(validate_mapping))
|
||||||
async def map_filter_to_code(config, filter_id):
|
async def map_filter_to_code(config, filter_id):
|
||||||
map_ = cg.std_ns.class_("map").template(cg.std_string, cg.std_string)
|
mappings = [
|
||||||
return cg.new_Pvariable(
|
cg.StructInitializer(
|
||||||
filter_id, map_([(item[CONF_FROM], item[CONF_TO]) for item in config])
|
cg.MockObj("Substitution", "esphome::text_sensor::"),
|
||||||
)
|
("from", conf[CONF_FROM]),
|
||||||
|
("to", conf[CONF_TO]),
|
||||||
|
)
|
||||||
|
for conf in config
|
||||||
|
]
|
||||||
|
return cg.new_Pvariable(filter_id, mappings)
|
||||||
|
|
||||||
|
|
||||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
|
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
|
||||||
|
|||||||
@@ -62,19 +62,36 @@ optional<std::string> AppendFilter::new_value(std::string value) { return value
|
|||||||
optional<std::string> PrependFilter::new_value(std::string value) { return this->prefix_ + value; }
|
optional<std::string> PrependFilter::new_value(std::string value) { return this->prefix_ + value; }
|
||||||
|
|
||||||
// Substitute
|
// Substitute
|
||||||
|
SubstituteFilter::SubstituteFilter(std::initializer_list<Substitution> substitutions) {
|
||||||
|
this->substitutions_.init(substitutions.size());
|
||||||
|
for (auto &sub : substitutions) {
|
||||||
|
this->substitutions_.push_back(std::move(sub));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
optional<std::string> SubstituteFilter::new_value(std::string value) {
|
optional<std::string> SubstituteFilter::new_value(std::string value) {
|
||||||
std::size_t pos;
|
std::size_t pos;
|
||||||
for (size_t i = 0; i < this->from_strings_.size(); i++) {
|
for (const auto &sub : this->substitutions_) {
|
||||||
while ((pos = value.find(this->from_strings_[i])) != std::string::npos)
|
while ((pos = value.find(sub.from)) != std::string::npos)
|
||||||
value.replace(pos, this->from_strings_[i].size(), this->to_strings_[i]);
|
value.replace(pos, sub.from.size(), sub.to);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map
|
// Map
|
||||||
|
MapFilter::MapFilter(std::initializer_list<Substitution> mappings) {
|
||||||
|
this->mappings_.init(mappings.size());
|
||||||
|
for (auto &mapping : mappings) {
|
||||||
|
this->mappings_.push_back(std::move(mapping));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
optional<std::string> MapFilter::new_value(std::string value) {
|
optional<std::string> MapFilter::new_value(std::string value) {
|
||||||
auto item = mappings_.find(value);
|
for (const auto &mapping : this->mappings_) {
|
||||||
return item == mappings_.end() ? value : item->second;
|
if (mapping.from == value)
|
||||||
|
return mapping.to;
|
||||||
|
}
|
||||||
|
return value; // Pass through if no match
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace text_sensor
|
} // namespace text_sensor
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include <queue>
|
|
||||||
#include <utility>
|
|
||||||
#include <map>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace text_sensor {
|
namespace text_sensor {
|
||||||
@@ -98,26 +94,52 @@ class PrependFilter : public Filter {
|
|||||||
std::string prefix_;
|
std::string prefix_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct Substitution {
|
||||||
|
std::string from;
|
||||||
|
std::string to;
|
||||||
|
};
|
||||||
|
|
||||||
/// A simple filter that replaces a substring with another substring
|
/// A simple filter that replaces a substring with another substring
|
||||||
class SubstituteFilter : public Filter {
|
class SubstituteFilter : public Filter {
|
||||||
public:
|
public:
|
||||||
SubstituteFilter(std::vector<std::string> from_strings, std::vector<std::string> to_strings)
|
explicit SubstituteFilter(std::initializer_list<Substitution> substitutions);
|
||||||
: from_strings_(std::move(from_strings)), to_strings_(std::move(to_strings)) {}
|
|
||||||
optional<std::string> new_value(std::string value) override;
|
optional<std::string> new_value(std::string value) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::vector<std::string> from_strings_;
|
FixedVector<Substitution> substitutions_;
|
||||||
std::vector<std::string> to_strings_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A filter that maps values from one set to another
|
/** A filter that maps values from one set to another
|
||||||
|
*
|
||||||
|
* Uses linear search instead of std::map for typical small datasets (2-20 mappings).
|
||||||
|
* Linear search on contiguous memory is faster than red-black tree lookups when:
|
||||||
|
* - Dataset is small (< ~30 items)
|
||||||
|
* - Memory is contiguous (cache-friendly, better CPU cache utilization)
|
||||||
|
* - No pointer chasing overhead (tree node traversal)
|
||||||
|
* - String comparison cost dominates lookup time
|
||||||
|
*
|
||||||
|
* Benchmark results (see benchmark_map_filter.cpp):
|
||||||
|
* - 2 mappings: Linear 1.26x faster than std::map
|
||||||
|
* - 5 mappings: Linear 2.25x faster than std::map
|
||||||
|
* - 10 mappings: Linear 1.83x faster than std::map
|
||||||
|
* - 20 mappings: Linear 1.59x faster than std::map
|
||||||
|
* - 30 mappings: Linear 1.09x faster than std::map
|
||||||
|
* - 40 mappings: std::map 1.27x faster than Linear (break-even)
|
||||||
|
*
|
||||||
|
* Benefits over std::map:
|
||||||
|
* - ~2KB smaller flash (no red-black tree code)
|
||||||
|
* - ~24-32 bytes less RAM per mapping (no tree node overhead)
|
||||||
|
* - Faster for typical ESPHome usage (2-10 mappings common, 20+ rare)
|
||||||
|
*
|
||||||
|
* Break-even point: ~35-40 mappings, but ESPHome configs rarely exceed 20
|
||||||
|
*/
|
||||||
class MapFilter : public Filter {
|
class MapFilter : public Filter {
|
||||||
public:
|
public:
|
||||||
MapFilter(std::map<std::string, std::string> mappings) : mappings_(std::move(mappings)) {}
|
explicit MapFilter(std::initializer_list<Substitution> mappings);
|
||||||
optional<std::string> new_value(std::string value) override;
|
optional<std::string> new_value(std::string value) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::map<std::string, std::string> mappings_;
|
FixedVector<Substitution> mappings_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace text_sensor
|
} // namespace text_sensor
|
||||||
|
|||||||
66
tests/components/text_sensor/common.yaml
Normal file
66
tests/components/text_sensor/common.yaml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
text_sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "Test Substitute Single"
|
||||||
|
id: test_substitute_single
|
||||||
|
filters:
|
||||||
|
- substitute:
|
||||||
|
- ERROR -> Error
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Substitute Multiple"
|
||||||
|
id: test_substitute_multiple
|
||||||
|
filters:
|
||||||
|
- substitute:
|
||||||
|
- ERROR -> Error
|
||||||
|
- WARN -> Warning
|
||||||
|
- INFO -> Information
|
||||||
|
- DEBUG -> Debug
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Substitute Chained"
|
||||||
|
id: test_substitute_chained
|
||||||
|
filters:
|
||||||
|
- substitute:
|
||||||
|
- foo -> bar
|
||||||
|
- to_upper
|
||||||
|
- substitute:
|
||||||
|
- BAR -> baz
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Map Single"
|
||||||
|
id: test_map_single
|
||||||
|
filters:
|
||||||
|
- map:
|
||||||
|
- ON -> Active
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Map Multiple"
|
||||||
|
id: test_map_multiple
|
||||||
|
filters:
|
||||||
|
- map:
|
||||||
|
- ON -> Active
|
||||||
|
- OFF -> Inactive
|
||||||
|
- UNKNOWN -> Error
|
||||||
|
- IDLE -> Standby
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Map Passthrough"
|
||||||
|
id: test_map_passthrough
|
||||||
|
filters:
|
||||||
|
- map:
|
||||||
|
- Good -> Excellent
|
||||||
|
- Bad -> Poor
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test All Filters"
|
||||||
|
id: test_all_filters
|
||||||
|
filters:
|
||||||
|
- to_upper
|
||||||
|
- to_lower
|
||||||
|
- append: " suffix"
|
||||||
|
- prepend: "prefix "
|
||||||
|
- substitute:
|
||||||
|
- prefix -> PREFIX
|
||||||
|
- suffix -> SUFFIX
|
||||||
|
- map:
|
||||||
|
- PREFIX text SUFFIX -> mapped
|
||||||
1
tests/components/text_sensor/test.esp8266-ard.yaml
Normal file
1
tests/components/text_sensor/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
||||||
Reference in New Issue
Block a user