mirror of
https://github.com/esphome/esphome.git
synced 2024-12-26 07:24:54 +01:00
String manipulation filters for text sensors (#2393)
* initial text sensor filter POC * fixed verbose logging * add append, prepend, substitute filters * add to lower, get to upper working without dummy * clang lint * more linting... * std::move append and prepend filters * fix verbose filter::input logging * value.c_str() in input print * lambda filter verbose log fix * correct log tag, neaten to upper and to lower * add on_raw_value automation/trigger
This commit is contained in:
parent
4f5e4f3b86
commit
3dfc8d4291
6 changed files with 373 additions and 5 deletions
|
@ -4,16 +4,22 @@ from esphome import automation
|
|||
from esphome.components import mqtt
|
||||
from esphome.const import (
|
||||
CONF_DISABLED_BY_DEFAULT,
|
||||
CONF_FILTERS,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_INTERNAL,
|
||||
CONF_ON_VALUE,
|
||||
CONF_ON_RAW_VALUE,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_NAME,
|
||||
CONF_STATE,
|
||||
CONF_FROM,
|
||||
CONF_TO,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.util import Registry
|
||||
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
|
@ -25,6 +31,9 @@ TextSensorPtr = TextSensor.operator("ptr")
|
|||
TextSensorStateTrigger = text_sensor_ns.class_(
|
||||
"TextSensorStateTrigger", automation.Trigger.template(cg.std_string)
|
||||
)
|
||||
TextSensorStateRawTrigger = text_sensor_ns.class_(
|
||||
"TextSensorStateRawTrigger", automation.Trigger.template(cg.std_string)
|
||||
)
|
||||
TextSensorPublishAction = text_sensor_ns.class_(
|
||||
"TextSensorPublishAction", automation.Action
|
||||
)
|
||||
|
@ -32,21 +41,101 @@ TextSensorStateCondition = text_sensor_ns.class_(
|
|||
"TextSensorStateCondition", automation.Condition
|
||||
)
|
||||
|
||||
FILTER_REGISTRY = Registry()
|
||||
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
|
||||
|
||||
# Filters
|
||||
Filter = text_sensor_ns.class_("Filter")
|
||||
LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter)
|
||||
ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter)
|
||||
ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter)
|
||||
AppendFilter = text_sensor_ns.class_("AppendFilter", Filter)
|
||||
PrependFilter = text_sensor_ns.class_("PrependFilter", Filter)
|
||||
SubstituteFilter = text_sensor_ns.class_("SubstituteFilter", Filter)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda)
|
||||
async def lambda_filter_to_code(config, filter_id):
|
||||
lambda_ = await cg.process_lambda(
|
||||
config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string)
|
||||
)
|
||||
return cg.new_Pvariable(filter_id, lambda_)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("to_upper", ToUpperFilter, {})
|
||||
async def to_upper_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(filter_id)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("to_lower", ToLowerFilter, {})
|
||||
async def to_lower_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(filter_id)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("append", AppendFilter, cv.string)
|
||||
async def append_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(filter_id, config)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("prepend", PrependFilter, cv.string)
|
||||
async def prepend_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(filter_id, config)
|
||||
|
||||
|
||||
def validate_substitute(value):
|
||||
if isinstance(value, dict):
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_FROM): cv.string,
|
||||
cv.Required(CONF_TO): cv.string,
|
||||
}
|
||||
)(value)
|
||||
value = cv.string(value)
|
||||
if "->" not in value:
|
||||
raise cv.Invalid("Substitute mapping must contain '->'")
|
||||
a, b = value.split("->", 1)
|
||||
a, b = a.strip(), b.strip()
|
||||
return validate_substitute({CONF_FROM: cv.string(a), CONF_TO: cv.string(b)})
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"substitute",
|
||||
SubstituteFilter,
|
||||
cv.All(cv.ensure_list(validate_substitute), cv.Length(min=2)),
|
||||
)
|
||||
async def substitute_filter_to_code(config, filter_id):
|
||||
from_strings = [conf[CONF_FROM] for conf in config]
|
||||
to_strings = [conf[CONF_TO] for conf in config]
|
||||
return cg.new_Pvariable(filter_id, from_strings, to_strings)
|
||||
|
||||
|
||||
icon = cv.icon
|
||||
|
||||
TEXT_SENSOR_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextSensor),
|
||||
cv.Optional(CONF_ICON): icon,
|
||||
cv.Optional(CONF_FILTERS): validate_filters,
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TextSensorStateTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
TextSensorStateRawTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def build_filters(config):
|
||||
return await cg.build_registry_list(FILTER_REGISTRY, config)
|
||||
|
||||
|
||||
async def setup_text_sensor_core_(var, config):
|
||||
cg.add(var.set_name(config[CONF_NAME]))
|
||||
cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT]))
|
||||
|
@ -55,10 +144,18 @@ async def setup_text_sensor_core_(var, config):
|
|||
if CONF_ICON in config:
|
||||
cg.add(var.set_icon(config[CONF_ICON]))
|
||||
|
||||
if config.get(CONF_FILTERS): # must exist and not be empty
|
||||
filters = await build_filters(config[CONF_FILTERS])
|
||||
cg.add(var.set_filters(filters))
|
||||
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_RAW_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
|
||||
|
||||
if CONF_MQTT_ID in config:
|
||||
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
|
|
|
@ -16,6 +16,13 @@ class TextSensorStateTrigger : public Trigger<std::string> {
|
|||
}
|
||||
};
|
||||
|
||||
class TextSensorStateRawTrigger : public Trigger<std::string> {
|
||||
public:
|
||||
explicit TextSensorStateRawTrigger(TextSensor *parent) {
|
||||
parent->add_on_raw_state_callback([this](std::string value) { this->trigger(std::move(value)); });
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class TextSensorStateCondition : public Condition<Ts...> {
|
||||
public:
|
||||
explicit TextSensorStateCondition(TextSensor *parent) : parent_(parent) {}
|
||||
|
|
74
esphome/components/text_sensor/filter.cpp
Normal file
74
esphome/components/text_sensor/filter.cpp
Normal file
|
@ -0,0 +1,74 @@
|
|||
#include "filter.h"
|
||||
#include "text_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace text_sensor {
|
||||
|
||||
static const char *const TAG = "text_sensor.filter";
|
||||
|
||||
// Filter
|
||||
void Filter::input(const std::string &value) {
|
||||
ESP_LOGVV(TAG, "Filter(%p)::input(%s)", this, value.c_str());
|
||||
optional<std::string> out = this->new_value(value);
|
||||
if (out.has_value())
|
||||
this->output(*out);
|
||||
}
|
||||
void Filter::output(const std::string &value) {
|
||||
if (this->next_ == nullptr) {
|
||||
ESP_LOGVV(TAG, "Filter(%p)::output(%s) -> SENSOR", this, value.c_str());
|
||||
this->parent_->internal_send_state_to_frontend(value);
|
||||
} else {
|
||||
ESP_LOGVV(TAG, "Filter(%p)::output(%s) -> %p", this, value.c_str(), this->next_);
|
||||
this->next_->input(value);
|
||||
}
|
||||
}
|
||||
void Filter::initialize(TextSensor *parent, Filter *next) {
|
||||
ESP_LOGVV(TAG, "Filter(%p)::initialize(parent=%p next=%p)", this, parent, next);
|
||||
this->parent_ = parent;
|
||||
this->next_ = next;
|
||||
}
|
||||
|
||||
// LambdaFilter
|
||||
LambdaFilter::LambdaFilter(lambda_filter_t lambda_filter) : lambda_filter_(std::move(lambda_filter)) {}
|
||||
const lambda_filter_t &LambdaFilter::get_lambda_filter() const { return this->lambda_filter_; }
|
||||
void LambdaFilter::set_lambda_filter(const lambda_filter_t &lambda_filter) { this->lambda_filter_ = lambda_filter; }
|
||||
|
||||
optional<std::string> LambdaFilter::new_value(std::string value) {
|
||||
auto it = this->lambda_filter_(value);
|
||||
ESP_LOGVV(TAG, "LambdaFilter(%p)::new_value(%s) -> %s", this, value.c_str(), it.value_or("").c_str());
|
||||
return it;
|
||||
}
|
||||
|
||||
// ToUpperFilter
|
||||
optional<std::string> ToUpperFilter::new_value(std::string value) {
|
||||
for (char &c : value)
|
||||
c = ::toupper(c);
|
||||
return value;
|
||||
}
|
||||
|
||||
// ToLowerFilter
|
||||
optional<std::string> ToLowerFilter::new_value(std::string value) {
|
||||
for (char &c : value)
|
||||
c = ::toupper(c);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Append
|
||||
optional<std::string> AppendFilter::new_value(std::string value) { return value + this->suffix_; }
|
||||
|
||||
// Prepend
|
||||
optional<std::string> PrependFilter::new_value(std::string value) { return this->prefix_ + value; }
|
||||
|
||||
// Substitute
|
||||
optional<std::string> SubstituteFilter::new_value(std::string value) {
|
||||
std::size_t pos;
|
||||
for (int i = 0; i < this->from_strings_.size(); i++)
|
||||
while ((pos = value.find(this->from_strings_[i])) != std::string::npos)
|
||||
value.replace(pos, this->from_strings_[i].size(), this->to_strings_[i]);
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace text_sensor
|
||||
} // namespace esphome
|
112
esphome/components/text_sensor/filter.h
Normal file
112
esphome/components/text_sensor/filter.h
Normal file
|
@ -0,0 +1,112 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <queue>
|
||||
#include <utility>
|
||||
|
||||
namespace esphome {
|
||||
namespace text_sensor {
|
||||
|
||||
class TextSensor;
|
||||
|
||||
/** Apply a filter to text sensor values such as to_upper.
|
||||
*
|
||||
* This class is purposefully kept quite simple, since more complicated
|
||||
* filters should really be done with the filter sensor in Home Assistant.
|
||||
*/
|
||||
class Filter {
|
||||
public:
|
||||
/** This will be called every time the filter receives a new value.
|
||||
*
|
||||
* It can return an empty optional to indicate that the filter chain
|
||||
* should stop, otherwise the value in the filter will be passed down
|
||||
* the chain.
|
||||
*
|
||||
* @param value The new value.
|
||||
* @return An optional string, the new value that should be pushed out.
|
||||
*/
|
||||
virtual optional<std::string> new_value(std::string value);
|
||||
|
||||
/// Initialize this filter, please note this can be called more than once.
|
||||
virtual void initialize(TextSensor *parent, Filter *next);
|
||||
|
||||
void input(const std::string &value);
|
||||
|
||||
void output(const std::string &value);
|
||||
|
||||
protected:
|
||||
friend TextSensor;
|
||||
|
||||
Filter *next_{nullptr};
|
||||
TextSensor *parent_{nullptr};
|
||||
};
|
||||
|
||||
using lambda_filter_t = std::function<optional<std::string>(std::string)>;
|
||||
|
||||
/** This class allows for creation of simple template filters.
|
||||
*
|
||||
* The constructor accepts a lambda of the form std::string -> optional<std::string>.
|
||||
* It will be called with each new value in the filter chain and returns the modified
|
||||
* value that shall be passed down the filter chain. Returning an empty Optional
|
||||
* means that the value shall be discarded.
|
||||
*/
|
||||
class LambdaFilter : public Filter {
|
||||
public:
|
||||
explicit LambdaFilter(lambda_filter_t lambda_filter);
|
||||
|
||||
optional<std::string> new_value(std::string value) override;
|
||||
|
||||
const lambda_filter_t &get_lambda_filter() const;
|
||||
void set_lambda_filter(const lambda_filter_t &lambda_filter);
|
||||
|
||||
protected:
|
||||
lambda_filter_t lambda_filter_;
|
||||
};
|
||||
|
||||
/// A simple filter that converts all text to uppercase
|
||||
class ToUpperFilter : public Filter {
|
||||
public:
|
||||
optional<std::string> new_value(std::string value) override;
|
||||
};
|
||||
|
||||
/// A simple filter that converts all text to lowercase
|
||||
class ToLowerFilter : public Filter {
|
||||
public:
|
||||
optional<std::string> new_value(std::string value) override;
|
||||
};
|
||||
|
||||
/// A simple filter that adds a string to the end of another string
|
||||
class AppendFilter : public Filter {
|
||||
public:
|
||||
AppendFilter(std::string suffix) : suffix_(std::move(suffix)) {}
|
||||
optional<std::string> new_value(std::string value) override;
|
||||
|
||||
protected:
|
||||
std::string suffix_;
|
||||
};
|
||||
|
||||
/// A simple filter that adds a string to the start of another string
|
||||
class PrependFilter : public Filter {
|
||||
public:
|
||||
PrependFilter(std::string prefix) : prefix_(std::move(prefix)) {}
|
||||
optional<std::string> new_value(std::string value) override;
|
||||
|
||||
protected:
|
||||
std::string prefix_;
|
||||
};
|
||||
|
||||
/// A simple filter that replaces a substring with another substring
|
||||
class SubstituteFilter : public Filter {
|
||||
public:
|
||||
SubstituteFilter(std::vector<std::string> from_strings, std::vector<std::string> to_strings)
|
||||
: from_strings_(std::move(from_strings)), to_strings_(std::move(to_strings)) {}
|
||||
optional<std::string> new_value(std::string value) override;
|
||||
|
||||
protected:
|
||||
std::vector<std::string> from_strings_;
|
||||
std::vector<std::string> to_strings_;
|
||||
};
|
||||
|
||||
} // namespace text_sensor
|
||||
} // namespace esphome
|
|
@ -10,21 +10,72 @@ TextSensor::TextSensor() : TextSensor("") {}
|
|||
TextSensor::TextSensor(const std::string &name) : Nameable(name) {}
|
||||
|
||||
void TextSensor::publish_state(const std::string &state) {
|
||||
this->state = state;
|
||||
this->raw_state = state;
|
||||
this->raw_callback_.call(state);
|
||||
|
||||
ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str());
|
||||
|
||||
if (this->filter_list_ == nullptr) {
|
||||
this->internal_send_state_to_frontend(state);
|
||||
} else {
|
||||
this->filter_list_->input(state);
|
||||
}
|
||||
}
|
||||
|
||||
void TextSensor::add_filter(Filter *filter) {
|
||||
// inefficient, but only happens once on every sensor setup and nobody's going to have massive amounts of
|
||||
// filters
|
||||
ESP_LOGVV(TAG, "TextSensor(%p)::add_filter(%p)", this, filter);
|
||||
if (this->filter_list_ == nullptr) {
|
||||
this->filter_list_ = filter;
|
||||
} else {
|
||||
Filter *last_filter = this->filter_list_;
|
||||
while (last_filter->next_ != nullptr)
|
||||
last_filter = last_filter->next_;
|
||||
last_filter->initialize(this, filter);
|
||||
}
|
||||
filter->initialize(this, nullptr);
|
||||
}
|
||||
void TextSensor::add_filters(const std::vector<Filter *> &filters) {
|
||||
for (Filter *filter : filters) {
|
||||
this->add_filter(filter);
|
||||
}
|
||||
}
|
||||
void TextSensor::set_filters(const std::vector<Filter *> &filters) {
|
||||
this->clear_filters();
|
||||
this->add_filters(filters);
|
||||
}
|
||||
void TextSensor::clear_filters() {
|
||||
if (this->filter_list_ != nullptr) {
|
||||
ESP_LOGVV(TAG, "TextSensor(%p)::clear_filters()", this);
|
||||
}
|
||||
this->filter_list_ = nullptr;
|
||||
}
|
||||
|
||||
void TextSensor::add_on_state_callback(std::function<void(std::string)> callback) {
|
||||
this->callback_.add(std::move(callback));
|
||||
}
|
||||
void TextSensor::add_on_raw_state_callback(std::function<void(std::string)> callback) {
|
||||
this->raw_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
std::string TextSensor::get_state() const { return this->state; }
|
||||
std::string TextSensor::get_raw_state() const { return this->raw_state; }
|
||||
void TextSensor::internal_send_state_to_frontend(const std::string &state) {
|
||||
this->state = this->raw_state;
|
||||
this->has_state_ = true;
|
||||
ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str());
|
||||
this->callback_.call(state);
|
||||
}
|
||||
|
||||
void TextSensor::set_icon(const std::string &icon) { this->icon_ = icon; }
|
||||
void TextSensor::add_on_state_callback(std::function<void(std::string)> callback) {
|
||||
this->callback_.add(std::move(callback));
|
||||
}
|
||||
std::string TextSensor::get_icon() {
|
||||
if (this->icon_.has_value())
|
||||
return *this->icon_;
|
||||
return this->icon();
|
||||
}
|
||||
std::string TextSensor::icon() { return ""; }
|
||||
|
||||
std::string TextSensor::unique_id() { return ""; }
|
||||
bool TextSensor::has_state() { return this->has_state_; }
|
||||
uint32_t TextSensor::hash_base() { return 334300109UL; }
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/text_sensor/filter.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace text_sensor {
|
||||
|
@ -22,13 +23,33 @@ class TextSensor : public Nameable {
|
|||
explicit TextSensor();
|
||||
explicit TextSensor(const std::string &name);
|
||||
|
||||
/// Getter-syntax for .state.
|
||||
std::string get_state() const;
|
||||
/// Getter-syntax for .raw_state
|
||||
std::string get_raw_state() const;
|
||||
|
||||
void publish_state(const std::string &state);
|
||||
|
||||
void set_icon(const std::string &icon);
|
||||
|
||||
/// Add a filter to the filter chain. Will be appended to the back.
|
||||
void add_filter(Filter *filter);
|
||||
|
||||
/// Add a list of vectors to the back of the filter chain.
|
||||
void add_filters(const std::vector<Filter *> &filters);
|
||||
|
||||
/// Clear the filters and replace them by filters.
|
||||
void set_filters(const std::vector<Filter *> &filters);
|
||||
|
||||
/// Clear the entire filter chain.
|
||||
void clear_filters();
|
||||
|
||||
void add_on_state_callback(std::function<void(std::string)> callback);
|
||||
/// Add a callback that will be called every time the sensor sends a raw value.
|
||||
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)
|
||||
|
@ -40,10 +61,16 @@ class TextSensor : public Nameable {
|
|||
|
||||
bool has_state();
|
||||
|
||||
void internal_send_state_to_frontend(const std::string &state);
|
||||
|
||||
protected:
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void(std::string)> callback_;
|
||||
CallbackManager<void(std::string)> raw_callback_; ///< Storage for raw state callbacks.
|
||||
CallbackManager<void(std::string)> callback_; ///< Storage for filtered state callbacks.
|
||||
|
||||
Filter *filter_list_{nullptr}; ///< Store all active filters.
|
||||
|
||||
optional<std::string> icon_;
|
||||
bool has_state_{false};
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue