mirror of
https://github.com/esphome/esphome.git
synced 2026-10-27 16:32:11 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8e2a04052 | ||
|
|
83d2e2efed | ||
|
|
c04cdea1a0 | ||
|
|
b1a2f7aacf | ||
|
|
2c11cfd1fa | ||
|
|
2c80263ddc | ||
|
|
050a5a2de0 | ||
|
|
46d8c3ec02 | ||
|
|
3259e72d9d | ||
|
|
ee59cf8974 | ||
|
|
29adc6f7e7 |
No files matched your search
@@ -443,7 +443,8 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
- **Hub forwarding entities need no C++ class:** a button, switch, number or select that only calls its hub
|
||||
uses `button.ApplyButton` (`switch.ApplySwitch`, `number.ApplyNumber`, `select.ApplySelect`) in its schema
|
||||
and `apply=` on the `sub_*` call. The body is a call on the hub with `entity` and `state`, `value` or
|
||||
`index`/`option` in scope; on a switch, number or select `publish="before"` or `"after"` publishes the state.
|
||||
`index`/`option` in scope; on a switch, number or select `publish="before"` or `"after"` publishes the state,
|
||||
and `publish="on_success"` publishes only when the hub call returns true (a write the device can refuse).
|
||||
`publish="momentary"` is only for legacy action switches (restart, factory reset): it publishes off and runs
|
||||
the body only when turned on. Never use it for a new entity; an action is a button.
|
||||
```python
|
||||
|
||||
@@ -59,7 +59,6 @@ from esphome.cpp_helpers import ( # noqa: F401
|
||||
add_variable,
|
||||
build_registry_entry,
|
||||
build_registry_list,
|
||||
build_registry_list_merged,
|
||||
extract_registry_entry_config,
|
||||
get_slot_count,
|
||||
gpio_pin_expression,
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
|
||||
#include "bq25186.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
class BQ25186PgGpoSwitch final : public switch_::Switch, public Parented<BQ25186Component> {
|
||||
protected:
|
||||
void write_state(bool state) override {
|
||||
if (this->parent_->write_pg_gpo_level(state))
|
||||
this->publish_state(state);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
#endif // USE_BQ25186_PG_GPO
|
||||
@@ -3,18 +3,16 @@ from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns
|
||||
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID
|
||||
|
||||
DEPENDENCIES = ["bq25186"]
|
||||
|
||||
CONF_PG_GPO = "pg_gpo"
|
||||
|
||||
BQ25186PgGpoSwitch = bq25186_ns.class_("BQ25186PgGpoSwitch", switch.Switch)
|
||||
|
||||
CONFIG_SCHEMA = BQ25186_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_PG_GPO): switch.switch_schema(
|
||||
BQ25186PgGpoSwitch, default_restore_mode="DISABLED"
|
||||
switch.ApplySwitch, default_restore_mode="DISABLED"
|
||||
),
|
||||
}
|
||||
)
|
||||
@@ -24,4 +22,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_BQ25186_PG_GPO")
|
||||
hub = await cg.get_variable(config[CONF_BQ25186_ID])
|
||||
switches = switch.sub_switches(config, parent=hub)
|
||||
await switches(CONF_PG_GPO, hub.set_pg_gpo_switch)
|
||||
await switches(
|
||||
CONF_PG_GPO,
|
||||
hub.set_pg_gpo_switch,
|
||||
apply="write_pg_gpo_level(state)",
|
||||
publish="on_success",
|
||||
)
|
||||
@@ -22,7 +22,7 @@ CONF_CUSTOM_PATTERNS = "custom_patterns"
|
||||
CONF_SKIP_CRC = "skip_crc"
|
||||
CONF_DEFAULT_OBIS = "default_obis"
|
||||
|
||||
DLMS_PARSER_VERSION = "2.2.2"
|
||||
DLMS_PARSER_VERSION = "2.2.0"
|
||||
|
||||
dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter")
|
||||
DlmsMeterComponent = dlms_meter_component_ns.class_(
|
||||
|
||||
@@ -2389,9 +2389,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_NAME): cv.string_strict,
|
||||
cv.Optional(CONF_SOURCE): cv.All(
|
||||
cv.string_strict, cv.Length(min=1)
|
||||
),
|
||||
cv.Optional(CONF_SOURCE): cv.git_ref,
|
||||
cv.Optional(CONF_REF): cv.string,
|
||||
cv.Optional(CONF_PATH): cv.string,
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
@@ -54,25 +53,16 @@ using DataCallbackManagerBase = CallbackManager<void(const AudioCapture &)>;
|
||||
|
||||
class DataCallbackManager : public DataCallbackManagerBase {
|
||||
public:
|
||||
DataCallbackManager(const audio::AudioStreamInfo *stream_info, const bool *mute_state)
|
||||
: stream_info_(stream_info), mute_state_(mute_state) {}
|
||||
explicit DataCallbackManager(const audio::AudioStreamInfo *stream_info) : stream_info_(stream_info) {}
|
||||
|
||||
using DataCallbackManagerBase::call;
|
||||
|
||||
// Remove before 2027.5.0, along with this class (use a plain CallbackManager)
|
||||
ESPDEPRECATED("Use Microphone::publish_audio_() instead. Removed in 2027.5.0", "2026.11.0")
|
||||
void call(const std::vector<uint8_t> &data) {
|
||||
if (*this->mute_state_) {
|
||||
const std::vector<uint8_t> zeros(data.size(), 0);
|
||||
this->call(AudioCapture{zeros, *this->stream_info_, 0});
|
||||
} else {
|
||||
this->call(AudioCapture{data, *this->stream_info_, 0});
|
||||
}
|
||||
}
|
||||
void call(const std::vector<uint8_t> &data) { this->call(AudioCapture{data, *this->stream_info_, 0}); }
|
||||
|
||||
protected:
|
||||
const audio::AudioStreamInfo *stream_info_;
|
||||
const bool *mute_state_;
|
||||
};
|
||||
|
||||
class Microphone {
|
||||
@@ -82,7 +72,15 @@ class Microphone {
|
||||
|
||||
/// Callbacks take (const AudioCapture &) and run on the microphone's task.
|
||||
template<typename F> void add_data_callback(F &&data_callback) {
|
||||
this->data_callbacks_.add(as_capture_callback(std::forward<F>(data_callback)));
|
||||
this->data_callbacks_.add(
|
||||
[this, callback = as_capture_callback(std::forward<F>(data_callback))](const AudioCapture &capture) {
|
||||
if (this->mute_state_) {
|
||||
const std::vector<uint8_t> zeros(capture.data.size(), 0);
|
||||
callback(AudioCapture{zeros, capture.stream_info, capture.capture_time_us});
|
||||
} else {
|
||||
callback(capture);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool is_running() const { return this->state_ == STATE_RUNNING; }
|
||||
@@ -94,11 +92,8 @@ class Microphone {
|
||||
audio::AudioStreamInfo get_audio_stream_info() { return this->audio_stream_info_; }
|
||||
|
||||
protected:
|
||||
/// While muted, data is zeroed first, so it must be a buffer the platform owns.
|
||||
void publish_audio_(std::span<uint8_t> data, const audio::AudioStreamInfo &stream_info, int64_t capture_time_us = 0) {
|
||||
if (this->mute_state_) {
|
||||
std::memset(data.data(), 0, data.size());
|
||||
}
|
||||
void publish_audio_(std::span<const uint8_t> data, const audio::AudioStreamInfo &stream_info,
|
||||
int64_t capture_time_us = 0) {
|
||||
this->data_callbacks_.call(AudioCapture{data, stream_info, capture_time_us});
|
||||
}
|
||||
|
||||
@@ -110,7 +105,7 @@ class Microphone {
|
||||
audio::AudioStreamInfo audio_stream_info_;
|
||||
|
||||
// Make private when the deprecated call(data) is removed in 2027.5.0
|
||||
DataCallbackManager data_callbacks_{&this->audio_stream_info_, &this->mute_state_};
|
||||
DataCallbackManager data_callbacks_{&this->audio_stream_info_};
|
||||
};
|
||||
|
||||
} // namespace esphome::microphone
|
||||
@@ -90,7 +90,6 @@ bool ResamplerMicrophone::init_resampler_(const audio::AudioStreamInfo &input_st
|
||||
|
||||
if (input_stream_info.get_sample_rate() == this->target_sample_rate_) {
|
||||
// The source already delivers the target sample rate, so its audio is passed through unchanged
|
||||
this->output_buffer_.reserve(input_stream_info.ms_to_bytes(BUFFER_DURATION_MS));
|
||||
this->input_stream_info_ = input_stream_info;
|
||||
this->resampler_ready_ = true;
|
||||
return true;
|
||||
@@ -138,23 +137,7 @@ void ResamplerMicrophone::process_audio_(const microphone::AudioCapture &capture
|
||||
}
|
||||
|
||||
if (this->resampler_ == nullptr) {
|
||||
if (this->mute_state_) {
|
||||
// The source's buffer is shared with its other consumers, so publish zeros from our own buffer, in pieces that
|
||||
// fit its reserved size so muting never allocates
|
||||
const uint32_t max_frames = input_stream_info.ms_to_frames(BUFFER_DURATION_MS);
|
||||
const uint32_t total_frames = input_stream_info.bytes_to_frames(capture.data.size());
|
||||
for (uint32_t done = 0; done < total_frames;) {
|
||||
const uint32_t frames = std::min(total_frames - done, max_frames);
|
||||
this->output_buffer_.assign(input_stream_info.frames_to_bytes(frames), 0);
|
||||
const int64_t time_us =
|
||||
capture.capture_time_us == 0 ? 0 : capture.capture_time_us + input_stream_info.frames_to_microseconds(done);
|
||||
this->publish_audio_(this->output_buffer_, input_stream_info, time_us);
|
||||
done += frames;
|
||||
}
|
||||
} else {
|
||||
// Needs a protected forwarding method once data_callbacks_ is made private in 2027.5.0
|
||||
this->data_callbacks_.call(microphone::AudioCapture{capture.data, input_stream_info, capture.capture_time_us});
|
||||
}
|
||||
this->publish_audio_(capture.data, input_stream_info, capture.capture_time_us);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ from esphome.const import (
|
||||
CONF_TIMEOUT,
|
||||
CONF_TO,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_USE_FAHRENHEIT,
|
||||
CONF_VALUE,
|
||||
@@ -1008,17 +1009,26 @@ def _timeout_throttle_period(
|
||||
return timeout[CONF_TIMEOUT]
|
||||
|
||||
|
||||
def _timeout_throttle_merge(first: ConfigType, second: ConfigType) -> tuple | None:
|
||||
if (period := _timeout_throttle_period(first, second)) is None:
|
||||
return None
|
||||
return TimeoutThrottleFilter, [period]
|
||||
|
||||
|
||||
async def _build_chain_filters(config: list[ConfigType]) -> list:
|
||||
"""Like build_filters, but merges the radar `timeout` + `throttle_with_priority` pair into one filter."""
|
||||
return await cg.build_registry_list_merged(
|
||||
FILTER_REGISTRY, config, _timeout_throttle_merge
|
||||
)
|
||||
filters = []
|
||||
i = 0
|
||||
while i < len(config):
|
||||
if (
|
||||
i + 1 < len(config)
|
||||
and (period := _timeout_throttle_period(config[i], config[i + 1]))
|
||||
is not None
|
||||
):
|
||||
filter_id = config[i][CONF_TYPE_ID].copy()
|
||||
filter_id.type = TimeoutThrottleFilter
|
||||
var = cg.new_Pvariable(filter_id, period)
|
||||
await cg.register_component(var, {})
|
||||
filters.append(var)
|
||||
i += 2
|
||||
continue
|
||||
filters.append(await cg.build_registry_entry(FILTER_REGISTRY, config[i]))
|
||||
i += 1
|
||||
return filters
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
|
||||
@@ -13,9 +13,7 @@ from esphome.const import (
|
||||
CONF_ON_RAW_VALUE,
|
||||
CONF_ON_VALUE,
|
||||
CONF_STATE,
|
||||
CONF_TIMEOUT,
|
||||
CONF_TO,
|
||||
CONF_VALUE,
|
||||
DEVICE_CLASS_DATE,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
@@ -61,18 +59,6 @@ AppendFilter = text_sensor_ns.class_("AppendFilter", Filter)
|
||||
PrependFilter = text_sensor_ns.class_("PrependFilter", Filter)
|
||||
SubstituteFilter = text_sensor_ns.class_("SubstituteFilter", Filter)
|
||||
MapFilter = text_sensor_ns.class_("MapFilter", Filter)
|
||||
ThrottleFilter = text_sensor_ns.class_("ThrottleFilter", Filter)
|
||||
ThrottleWithPriorityFilter = text_sensor_ns.class_(
|
||||
"ThrottleWithPriorityFilter", ThrottleFilter
|
||||
)
|
||||
TimeoutFilterBase = text_sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component)
|
||||
TimeoutFilterLast = text_sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase)
|
||||
TimeoutFilterConfigured = text_sensor_ns.class_(
|
||||
"TimeoutFilterConfigured", TimeoutFilterBase
|
||||
)
|
||||
TimeoutThrottleFilter = text_sensor_ns.class_(
|
||||
"TimeoutThrottleFilter", TimeoutFilterLast
|
||||
)
|
||||
Substitution = text_sensor_ns.struct("Substitution")
|
||||
|
||||
|
||||
@@ -142,80 +128,6 @@ async def substitution_filter_to_code(
|
||||
return cg.new_Pvariable(filter_id, _substitution_table(config))
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"throttle", ThrottleFilter, cv.positive_time_period_milliseconds
|
||||
)
|
||||
async def throttle_filter_to_code(config: ConfigType, filter_id: ID) -> MockObj:
|
||||
return cg.new_Pvariable(filter_id, config)
|
||||
|
||||
|
||||
THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_VALUE, default=[]): cv.ensure_list(cv.string),
|
||||
},
|
||||
key=CONF_TIMEOUT,
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"throttle_with_priority", ThrottleWithPriorityFilter, THROTTLE_WITH_PRIORITY_SCHEMA
|
||||
)
|
||||
async def throttle_with_priority_filter_to_code(
|
||||
config: ConfigType, filter_id: ID
|
||||
) -> MockObj:
|
||||
if not (values := config[CONF_VALUE]):
|
||||
filter_id = filter_id.copy()
|
||||
filter_id.type = ThrottleFilter
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT])
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], _priority_table(values))
|
||||
|
||||
|
||||
def _priority_table(values: list[str]) -> MockObj:
|
||||
"""Shared PROGMEM list of priority texts, ended by nullptr; nullptr without any."""
|
||||
if not values:
|
||||
return cg.nullptr
|
||||
return cg.shared_progmem_array(
|
||||
"text_sensor_priorities",
|
||||
cg.const_char_ptr,
|
||||
cg.ArrayInitializer(*values, cg.nullptr),
|
||||
)
|
||||
|
||||
|
||||
def timeout_throttle_filters(priority: str, period_ms: int = 1000) -> list[ConfigType]:
|
||||
"""Default filters for a text entity: resend the last value after a pause, at most one value a period,
|
||||
`priority` at once. Built as one filter, like sensor.TIMEOUT_THROTTLE_FILTERS."""
|
||||
period = cv.TimePeriod(milliseconds=period_ms)
|
||||
return [
|
||||
{"timeout": {CONF_TIMEOUT: period, CONF_VALUE: "last"}},
|
||||
{"throttle_with_priority": {CONF_TIMEOUT: period, CONF_VALUE: [priority]}},
|
||||
]
|
||||
|
||||
|
||||
TIMEOUT_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_VALUE, default="last"): cv.string,
|
||||
},
|
||||
key=CONF_TIMEOUT,
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("timeout", TimeoutFilterBase, TIMEOUT_SCHEMA)
|
||||
async def timeout_filter_to_code(config: ConfigType, filter_id: ID) -> MockObj:
|
||||
filter_id = filter_id.copy()
|
||||
args = [config[CONF_TIMEOUT]]
|
||||
# "last" is a keyword, so a timeout cannot send that literal text
|
||||
if (value := config[CONF_VALUE]) == "last":
|
||||
filter_id.type = TimeoutFilterLast
|
||||
else:
|
||||
filter_id.type = TimeoutFilterConfigured
|
||||
args.append(value)
|
||||
var = cg.new_Pvariable(filter_id, *args)
|
||||
await cg.register_component(var, {})
|
||||
return var
|
||||
|
||||
|
||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
|
||||
|
||||
_TEXT_SENSOR_SCHEMA = (
|
||||
@@ -272,32 +184,6 @@ async def build_filters(config):
|
||||
return await cg.build_registry_list(FILTER_REGISTRY, config)
|
||||
|
||||
|
||||
def _timeout_throttle_pair(first: ConfigType, second: ConfigType) -> tuple | None:
|
||||
"""Period and priority texts of a `timeout` (value `last`) directly followed by a
|
||||
`throttle_with_priority` with the same period, or None."""
|
||||
if (timeout := first.get(CONF_TIMEOUT)) is None or (
|
||||
throttle := second.get("throttle_with_priority")
|
||||
) is None:
|
||||
return None
|
||||
if timeout[CONF_VALUE] != "last" or timeout[CONF_TIMEOUT] != throttle[CONF_TIMEOUT]:
|
||||
return None
|
||||
return timeout[CONF_TIMEOUT], throttle[CONF_VALUE]
|
||||
|
||||
|
||||
def _timeout_throttle_merge(first: ConfigType, second: ConfigType) -> tuple | None:
|
||||
if (pair := _timeout_throttle_pair(first, second)) is None:
|
||||
return None
|
||||
period, values = pair
|
||||
return TimeoutThrottleFilter, [period, _priority_table(values)]
|
||||
|
||||
|
||||
async def _build_chain_filters(config: list[ConfigType]) -> list:
|
||||
"""Like build_filters, but merges a `timeout` + `throttle_with_priority` pair into one filter."""
|
||||
return await cg.build_registry_list_merged(
|
||||
FILTER_REGISTRY, config, _timeout_throttle_merge
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_VALUE, "add_on_state_callback", [(cg.std_string, "x")]
|
||||
@@ -319,7 +205,7 @@ async def setup_text_sensor_core_(var, config):
|
||||
|
||||
if config.get(CONF_FILTERS): # must exist and not be empty
|
||||
cg.add_define("USE_TEXT_SENSOR_FILTER")
|
||||
filters = await _build_chain_filters(config[CONF_FILTERS])
|
||||
filters = await build_filters(config[CONF_FILTERS])
|
||||
for filter_ in filters:
|
||||
cg.add(var.add_filter(filter_))
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "filter.h"
|
||||
#include "text_sensor.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
@@ -99,65 +98,6 @@ bool MapFilter::new_value(std::string &value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Throttle
|
||||
// True when a value may pass: the period is over, or the value is in the nullptr-ended `prioritized` list
|
||||
static bool throttle_passes(uint32_t &last_input, uint32_t period, const std::string &value,
|
||||
const char *const *prioritized) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
bool pass = last_input == 0 || now - last_input >= period;
|
||||
for (const char *const *p = prioritized; !pass && p != nullptr && *p != nullptr; p++)
|
||||
pass = value == *p;
|
||||
if (pass)
|
||||
last_input = now;
|
||||
return pass;
|
||||
}
|
||||
|
||||
bool ThrottleFilter::new_value(std::string &value) {
|
||||
return throttle_passes(this->last_input_, this->min_time_between_inputs_, value, nullptr);
|
||||
}
|
||||
|
||||
bool ThrottleWithPriorityFilter::new_value(std::string &value) {
|
||||
return throttle_passes(this->last_input_, this->min_time_between_inputs_, value, this->prioritized_);
|
||||
}
|
||||
|
||||
// Timeout
|
||||
bool TimeoutFilterBase::new_value(std::string &value) {
|
||||
this->timeout_start_time_ = App.get_loop_component_start_time();
|
||||
this->enable_loop();
|
||||
return true;
|
||||
}
|
||||
|
||||
void TimeoutFilterBase::loop() {
|
||||
if (App.get_loop_component_start_time() - this->timeout_start_time_ < this->time_period_)
|
||||
return;
|
||||
// Before sending, so a callback that publishes to this sensor again re-arms the timeout
|
||||
this->disable_loop();
|
||||
this->send_timeout_value();
|
||||
}
|
||||
|
||||
void TimeoutFilterBase::send_(const char *text, size_t len) {
|
||||
if (this->next_ == nullptr) {
|
||||
this->parent_->internal_send_state_to_frontend(text, len);
|
||||
} else {
|
||||
this->next_->input(std::string(text, len));
|
||||
}
|
||||
}
|
||||
|
||||
bool TimeoutFilterLast::new_value(std::string &value) {
|
||||
this->last_ = value; // reuses last_'s buffer
|
||||
return TimeoutFilterBase::new_value(value);
|
||||
}
|
||||
|
||||
bool TimeoutThrottleFilter::new_value(std::string &value) {
|
||||
TimeoutFilterLast::new_value(value);
|
||||
return throttle_passes(this->last_input_, this->time_period_, value, this->prioritized_);
|
||||
}
|
||||
|
||||
void TimeoutThrottleFilter::send_timeout_value() {
|
||||
if (throttle_passes(this->last_input_, this->time_period_, this->last_, this->prioritized_))
|
||||
this->send_(this->last_.data(), this->last_.size());
|
||||
}
|
||||
|
||||
} // namespace esphome::text_sensor
|
||||
|
||||
#endif // USE_TEXT_SENSOR_FILTER
|
||||
@@ -169,82 +169,6 @@ class MapFilter : public Filter {
|
||||
const Substitution *mappings_;
|
||||
};
|
||||
|
||||
/// Drops a value that comes sooner than `min_time_between_inputs` after the last one let through.
|
||||
class ThrottleFilter : public Filter {
|
||||
public:
|
||||
explicit ThrottleFilter(uint32_t min_time_between_inputs) : min_time_between_inputs_(min_time_between_inputs) {}
|
||||
bool new_value(std::string &value) override;
|
||||
|
||||
protected:
|
||||
uint32_t last_input_{0};
|
||||
uint32_t min_time_between_inputs_;
|
||||
};
|
||||
|
||||
/// Same as ThrottleFilter, but a value in `prioritized` always passes. `prioritized` is a codegen table
|
||||
/// in flash, shared by filters with the same list and ended by nullptr.
|
||||
class ThrottleWithPriorityFilter : public ThrottleFilter {
|
||||
public:
|
||||
ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, const char *const *prioritized)
|
||||
: ThrottleFilter(min_time_between_inputs), prioritized_(prioritized) {}
|
||||
bool new_value(std::string &value) override;
|
||||
|
||||
protected:
|
||||
const char *const *prioritized_;
|
||||
};
|
||||
|
||||
/// Passes every value on and, once none has come for `time_period`, sends one value again. A Component rather than
|
||||
/// a scheduler timeout for the reason given on the sensor's TimeoutFilterBase: it re-arms on every value.
|
||||
class TimeoutFilterBase : public Filter, public Component {
|
||||
public:
|
||||
bool new_value(std::string &value) override;
|
||||
void loop() override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
protected:
|
||||
explicit TimeoutFilterBase(uint32_t time_period) : time_period_(time_period) { this->disable_loop(); }
|
||||
/// Sends `text`; never moves a stored string on, so it keeps its buffer.
|
||||
void send_(const char *text, size_t len);
|
||||
virtual void send_timeout_value() = 0;
|
||||
|
||||
uint32_t time_period_;
|
||||
uint32_t timeout_start_time_{0};
|
||||
};
|
||||
|
||||
/// Sends the last value again.
|
||||
class TimeoutFilterLast : public TimeoutFilterBase {
|
||||
public:
|
||||
explicit TimeoutFilterLast(uint32_t time_period) : TimeoutFilterBase(time_period) {}
|
||||
bool new_value(std::string &value) override;
|
||||
|
||||
protected:
|
||||
void send_timeout_value() override { this->send_(this->last_.data(), this->last_.size()); }
|
||||
std::string last_;
|
||||
};
|
||||
|
||||
/// Codegen use only: `timeout` (value `last`) followed by `throttle_with_priority` with the same period, as one
|
||||
/// object. Output matches the two filter chain exactly.
|
||||
class TimeoutThrottleFilter : public TimeoutFilterLast {
|
||||
public:
|
||||
TimeoutThrottleFilter(uint32_t time_period, const char *const *prioritized)
|
||||
: TimeoutFilterLast(time_period), prioritized_(prioritized) {}
|
||||
bool new_value(std::string &value) override;
|
||||
|
||||
protected:
|
||||
void send_timeout_value() override;
|
||||
const char *const *prioritized_;
|
||||
uint32_t last_input_{0}; // When the throttle stage last let a value through
|
||||
};
|
||||
|
||||
/// Sends the configured text.
|
||||
class TimeoutFilterConfigured : public TimeoutFilterBase {
|
||||
public:
|
||||
TimeoutFilterConfigured(uint32_t time_period, const char *value) : TimeoutFilterBase(time_period), value_(value) {}
|
||||
|
||||
protected:
|
||||
void send_timeout_value() override { this->send_(this->value_, strlen(this->value_)); }
|
||||
const char *value_;
|
||||
};
|
||||
|
||||
} // namespace esphome::text_sensor
|
||||
|
||||
#endif // USE_TEXT_SENSOR_FILTER
|
||||
@@ -2744,13 +2744,8 @@ def url(value):
|
||||
raise Invalid("Expected a file scheme or a URL scheme with host")
|
||||
|
||||
|
||||
# Characters git never allows in a branch or tag name (git check-ref-format)
|
||||
GIT_REF_PATTERN = r"[^\s~^:?*\[\\]+"
|
||||
|
||||
|
||||
def git_ref(value: typing.Any) -> str:
|
||||
value = string_strict(value)
|
||||
if re.fullmatch(GIT_REF_PATTERN, value) is None:
|
||||
def git_ref(value):
|
||||
if re.match(r"[a-zA-Z0-9_./-]+", value) is None:
|
||||
raise Invalid("Not a valid git ref")
|
||||
return value
|
||||
|
||||
@@ -3005,8 +3000,9 @@ def validate_source_shorthand(value):
|
||||
except Invalid:
|
||||
pass
|
||||
# Regex for GitHub repo name with optional branch/tag
|
||||
m = re.fullmatch(
|
||||
rf"github://(?:([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@({GIT_REF_PATTERN}))?|pr#([0-9]+))",
|
||||
# Note: git allows other branch/tag names as well, but never seen them used before
|
||||
m = re.match(
|
||||
r"github://(?:([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@([a-zA-Z0-9\-_.\./]+))?|pr#([0-9]+))",
|
||||
value,
|
||||
)
|
||||
if m is None:
|
||||
|
||||
@@ -5,7 +5,7 @@ from importlib import import_module
|
||||
from importlib.util import find_spec
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Literal
|
||||
from typing import Any, Literal, get_args
|
||||
|
||||
from esphome.automation import apply_static_function, parent_ref
|
||||
import esphome.codegen as cg
|
||||
@@ -503,7 +503,8 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) ->
|
||||
config[_KEY_ICON_IDX] = icon_idx
|
||||
|
||||
|
||||
ApplyPublish = Literal["before", "after", "momentary"] | None
|
||||
ApplyPublishMode = Literal["before", "after", "momentary", "on_success"]
|
||||
ApplyPublish = ApplyPublishMode | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -577,7 +578,8 @@ class SubEntities:
|
||||
|
||||
With ``apply`` the entity is the domain's ``Apply*`` class and its control runs
|
||||
``parent->apply``, or ``apply`` as a namespaced free statement when no parent is bound, with
|
||||
``publish`` ("before"/"after") placing the domain's publish. ``publish="momentary"`` instead
|
||||
``publish`` ("before"/"after") placing the domain's publish; ``"on_success"`` publishes only when
|
||||
``apply`` returns true. ``publish="momentary"`` instead
|
||||
acknowledges first (a switch publishes off) and runs ``apply`` only when turned on. It is only for
|
||||
legacy action switches; a new action entity is a button.
|
||||
"""
|
||||
@@ -600,10 +602,8 @@ class SubEntities:
|
||||
)
|
||||
if publish is not None and (apply is None or self.apply_entity.publish is None):
|
||||
raise ValueError(f"{entity_id}: publish= needs apply= on a stateful domain")
|
||||
if publish not in (None, "before", "after", "momentary"):
|
||||
raise ValueError(
|
||||
f'{entity_id}: publish= must be "before", "after" or "momentary"'
|
||||
)
|
||||
if publish is not None and publish not in (modes := get_args(ApplyPublishMode)):
|
||||
raise ValueError(f"{entity_id}: publish= must be one of {', '.join(modes)}")
|
||||
if publish == "momentary" and self.apply_entity.momentary is None:
|
||||
raise ValueError(f'{entity_id}: publish="momentary" is not supported here')
|
||||
|
||||
@@ -626,11 +626,14 @@ class SubEntities:
|
||||
if isinstance(parent, ID):
|
||||
parent = await get_variable(parent)
|
||||
apply_entity = self.apply_entity
|
||||
call = f"{apply};" if parent is None else f"{parent_ref(parent)}->{apply};"
|
||||
expr = apply if parent is None else f"{parent_ref(parent)}->{apply}"
|
||||
call = f"{expr};"
|
||||
if publish == "before":
|
||||
body = [apply_entity.publish, call]
|
||||
elif publish == "after":
|
||||
body = [call, apply_entity.publish]
|
||||
elif publish == "on_success":
|
||||
body = [f"if ({expr}) {{ {apply_entity.publish} }}"]
|
||||
elif publish == "momentary":
|
||||
acknowledge, condition = apply_entity.momentary
|
||||
body = [acknowledge, f"if ({condition}) {{ {call} }}"]
|
||||
|
||||
@@ -372,31 +372,6 @@ async def build_registry_list(registry, config):
|
||||
return actions
|
||||
|
||||
|
||||
async def build_registry_list_merged(
|
||||
registry: Registry,
|
||||
config: list[ConfigType],
|
||||
merge: Callable[[ConfigType, ConfigType], tuple[Any, list] | None],
|
||||
) -> list[MockObj]:
|
||||
"""Like build_registry_list, but two neighbouring entries that ``merge`` accepts are built as one Component.
|
||||
|
||||
``merge(first, second)`` returns the merged class and its constructor arguments, or None.
|
||||
"""
|
||||
built = []
|
||||
i = 0
|
||||
while i < len(config):
|
||||
if i + 1 < len(config) and (merged := merge(config[i], config[i + 1])):
|
||||
type_id = config[i][CONF_TYPE_ID].copy()
|
||||
type_id.type, args = merged
|
||||
var = new_Pvariable(type_id, *args)
|
||||
await register_component(var, {})
|
||||
built.append(var)
|
||||
i += 2
|
||||
continue
|
||||
built.append(await build_registry_entry(registry, config[i]))
|
||||
i += 1
|
||||
return built
|
||||
|
||||
|
||||
async def past_safe_mode():
|
||||
if CONF_SAFE_MODE not in CORE.config:
|
||||
return None
|
||||
|
||||
+2
-2
@@ -1051,8 +1051,8 @@ class GitFile:
|
||||
"""Parse a git shorthand URL into its components."""
|
||||
if not isinstance(shorthand, str):
|
||||
raise ValueError("Git shorthand must be a string")
|
||||
m = re.fullmatch(
|
||||
rf"(?P<domain>[a-zA-Z0-9\-]+)://(?P<owner>[a-zA-Z0-9\-]+)/(?P<repo>[a-zA-Z0-9\-\._]+)/(?P<filename>[a-zA-Z0-9\-_.\./]+)(?:@(?P<ref>{cv.GIT_REF_PATTERN}))?(?:\?(?P<query>[a-zA-Z0-9\-_.\./]+))?",
|
||||
m = re.match(
|
||||
r"(?P<domain>[a-zA-Z0-9\-]+)://(?P<owner>[a-zA-Z0-9\-]+)/(?P<repo>[a-zA-Z0-9\-\._]+)/(?P<filename>[a-zA-Z0-9\-_.\./]+)(?:@(?P<ref>[a-zA-Z0-9\-_.\./]+))?(?:\?(?P<query>[a-zA-Z0-9\-_.\./]+))?",
|
||||
shorthand,
|
||||
)
|
||||
if m is None:
|
||||
|
||||
@@ -2,7 +2,7 @@ dependencies:
|
||||
bblanchon/arduinojson:
|
||||
version: "7.4.3"
|
||||
esphome/dlms_parser:
|
||||
version: 2.2.2
|
||||
version: 2.2.0
|
||||
esphome/esp-audio-libs:
|
||||
version: 4.1.0
|
||||
esphome/esp-micro-speech-features:
|
||||
|
||||
+2
-7
@@ -284,17 +284,12 @@ def _lookup_module(domain: str, exception: bool) -> ComponentManifest | None:
|
||||
_COMPONENT_CACHE[domain] = manif
|
||||
return manif
|
||||
|
||||
module_name = f"esphome.components.{domain}"
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
module = importlib.import_module(f"esphome.components.{domain}")
|
||||
except ImportError as e:
|
||||
if exception:
|
||||
raise
|
||||
if (
|
||||
isinstance(e, ModuleNotFoundError)
|
||||
and e.name is not None
|
||||
and f"{module_name}.".startswith(f"{e.name}.")
|
||||
):
|
||||
if "No module named" in str(e):
|
||||
_LOGGER.info(
|
||||
"Unable to import component %s: %s", domain, str(e), exc_info=False
|
||||
)
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ build_unflags =
|
||||
; esp32 envs (which would double-include them).
|
||||
[common:idf-component-libs]
|
||||
lib_deps =
|
||||
esphome/dlms_parser@2.2.2 ; dlms_meter
|
||||
esphome/dlms_parser@2.2.0 ; dlms_meter
|
||||
bblanchon/ArduinoJson@7.4.3 ; json
|
||||
lvgl/lvgl@9.6.0 ; lvgl
|
||||
|
||||
|
||||
@@ -100,43 +100,3 @@ def test_substitution_table_names_avoid_user_ids(
|
||||
assert len(tables) == 2
|
||||
assert "text_sensor_substitutions" not in tables
|
||||
assert "text_sensor_mapfilter_id_table" not in tables
|
||||
|
||||
|
||||
def test_throttle_and_timeout_filters(generate_main: Callable[[str], str]) -> None:
|
||||
"""Priority values go in a PROGMEM table ended by nullptr; no priority is a plain throttle."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/text_sensor/test_text_sensor_throttle_timeout.yaml"
|
||||
)
|
||||
|
||||
table = re.search(
|
||||
r'static constexpr const char \* (\w+)\[\] PROGMEM = \{"NA", nullptr\}',
|
||||
main_cpp,
|
||||
)
|
||||
assert table is not None
|
||||
# timeout (last) then throttle_with_priority with the same period merge into one filter
|
||||
assert f"text_sensor::TimeoutThrottleFilter(1000, {table.group(1)})" in main_cpp
|
||||
alone = re.search(
|
||||
r'static constexpr const char \* (\w+)\[\] PROGMEM = \{"Off", "NA", nullptr\}',
|
||||
main_cpp,
|
||||
)
|
||||
assert alone is not None
|
||||
assert f"text_sensor::ThrottleWithPriorityFilter(300, {alone.group(1)})" in main_cpp
|
||||
assert "text_sensor::ThrottleFilter(500)" in main_cpp
|
||||
assert "text_sensor::TimeoutThrottleFilter(2000, nullptr)" in main_cpp
|
||||
assert 'text_sensor::TimeoutFilterConfigured(30000, "Offline")' in main_cpp
|
||||
|
||||
|
||||
def test_timeout_throttle_filters_default_merges() -> None:
|
||||
"""The validated default is a pair the chain builder merges into one filter."""
|
||||
from esphome.components.text_sensor import (
|
||||
_timeout_throttle_pair,
|
||||
timeout_throttle_filters,
|
||||
validate_filters,
|
||||
)
|
||||
|
||||
first, second = validate_filters(timeout_throttle_filters("NA"))
|
||||
pair = _timeout_throttle_pair(first, second)
|
||||
assert pair is not None
|
||||
period, values = pair
|
||||
assert period.total_milliseconds == 1000
|
||||
assert values == ["NA"]
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
text_sensor:
|
||||
- platform: template
|
||||
id: ts_position
|
||||
filters:
|
||||
- timeout: 1s
|
||||
- throttle_with_priority:
|
||||
timeout: 1s
|
||||
value: NA
|
||||
- platform: template
|
||||
id: ts_plain
|
||||
filters:
|
||||
- throttle_with_priority: 500ms
|
||||
- timeout:
|
||||
timeout: 30s
|
||||
value: Offline
|
||||
- platform: template
|
||||
id: ts_merged_plain
|
||||
filters:
|
||||
- timeout: 2s
|
||||
- throttle_with_priority: 2s
|
||||
- platform: template
|
||||
id: ts_priority_alone
|
||||
filters:
|
||||
- throttle_with_priority:
|
||||
timeout: 300ms
|
||||
value:
|
||||
- "Off"
|
||||
- NA
|
||||
@@ -0,0 +1,14 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# One empty config entry so to_code runs and compiles the PG/GPO switch path.
|
||||
manifest.multi_conf = False
|
||||
manifest.dependencies = manifest.dependencies + ["i2c", "switch"]
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_BQ25186_PG_GPO")
|
||||
|
||||
manifest.to_code = to_code
|
||||
@@ -0,0 +1,143 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "esphome/components/bq25186/bq25186.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
namespace esphome::bq25186::testing {
|
||||
|
||||
static constexpr uint8_t ADDRESS = 0x6A;
|
||||
static constexpr uint8_t SYS_REG_OTHER_BITS = 0x0F;
|
||||
|
||||
// The chip's registers behind an address pointer that moves on after every byte; writes can be made to fail.
|
||||
class RegisterBus final : public i2c::I2CBus {
|
||||
public:
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *data, size_t size, uint8_t *out,
|
||||
size_t out_size) override {
|
||||
EXPECT_EQ(address, ADDRESS);
|
||||
if (size > 1 && this->fail_writes)
|
||||
return i2c::ERROR_NOT_ACKNOWLEDGED;
|
||||
if (size != 0) {
|
||||
this->pointer_ = data[0];
|
||||
for (size_t i = 1; i < size; i++)
|
||||
this->registers[this->next_()] = data[i];
|
||||
}
|
||||
for (size_t i = 0; i < out_size; i++)
|
||||
out[i] = this->registers[this->next_()];
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
std::array<uint8_t, BQ25186_STATUS_REGISTER_COUNT> registers{};
|
||||
bool fail_writes{false};
|
||||
|
||||
protected:
|
||||
size_t next_() { return this->pointer_++ % this->registers.size(); }
|
||||
|
||||
uint8_t pointer_{0};
|
||||
};
|
||||
|
||||
// The body code generation emits for apply= with publish="on_success"
|
||||
static BQ25186Component *hub_under_test = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static void apply_pg_gpo(switch_::Switch *entity, bool state) {
|
||||
if (hub_under_test->write_pg_gpo_level(state))
|
||||
entity->publish_state(state);
|
||||
}
|
||||
|
||||
class BQ25186PgGpoTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->bus_.registers[BQ25186_REG_MASK_ID] = 0x01; // the BQ25186 device id
|
||||
this->bus_.registers[BQ25186_REG_SYS_REG] = SYS_REG_OTHER_BITS;
|
||||
this->hub_.set_i2c_bus(&this->bus_);
|
||||
this->hub_.set_i2c_address(ADDRESS);
|
||||
hub_under_test = &this->hub_;
|
||||
this->switch_.set_restore_mode(switch_::SWITCH_RESTORE_DISABLED);
|
||||
this->hub_.set_pg_gpo_switch(&this->switch_);
|
||||
}
|
||||
|
||||
bool level_() const { return (this->bus_.registers[BQ25186_REG_SYS_REG] & BQ25186_SYS_REG_PG_GPO_LEVEL) != 0; }
|
||||
uint8_t other_bits_() const { return this->bus_.registers[BQ25186_REG_SYS_REG] & ~BQ25186_SYS_REG_PG_GPO_LEVEL; }
|
||||
|
||||
RegisterBus bus_;
|
||||
BQ25186Component hub_;
|
||||
switch_::ApplySwitch<apply_pg_gpo> switch_;
|
||||
};
|
||||
|
||||
TEST_F(BQ25186PgGpoTest, TurnOnSetsTheLevelAndPublishes) {
|
||||
this->switch_.turn_on();
|
||||
EXPECT_TRUE(this->level_());
|
||||
EXPECT_EQ(this->other_bits_(), SYS_REG_OTHER_BITS);
|
||||
EXPECT_TRUE(this->switch_.has_state());
|
||||
EXPECT_TRUE(this->switch_.state);
|
||||
}
|
||||
|
||||
TEST_F(BQ25186PgGpoTest, TurnOffClearsTheLevelAndPublishes) {
|
||||
this->bus_.registers[BQ25186_REG_SYS_REG] |= BQ25186_SYS_REG_PG_GPO_LEVEL;
|
||||
this->switch_.publish_state(true);
|
||||
this->switch_.turn_off();
|
||||
EXPECT_FALSE(this->level_());
|
||||
EXPECT_EQ(this->other_bits_(), SYS_REG_OTHER_BITS);
|
||||
EXPECT_FALSE(this->switch_.state);
|
||||
}
|
||||
|
||||
TEST_F(BQ25186PgGpoTest, FailedWriteLeavesTheStateUnpublished) {
|
||||
this->bus_.fail_writes = true;
|
||||
this->switch_.turn_on();
|
||||
EXPECT_FALSE(this->level_());
|
||||
EXPECT_FALSE(this->switch_.has_state());
|
||||
}
|
||||
|
||||
TEST_F(BQ25186PgGpoTest, FailedWriteKeepsThePreviousState) {
|
||||
// Start from on, so a reset to the default off state would show
|
||||
this->bus_.registers[BQ25186_REG_SYS_REG] |= BQ25186_SYS_REG_PG_GPO_LEVEL;
|
||||
this->switch_.publish_state(true);
|
||||
this->bus_.fail_writes = true;
|
||||
this->switch_.turn_off();
|
||||
EXPECT_TRUE(this->level_());
|
||||
EXPECT_TRUE(this->switch_.state);
|
||||
}
|
||||
|
||||
TEST_F(BQ25186PgGpoTest, InvertedSwitchDrivesTheOppositeLevel) {
|
||||
this->switch_.set_inverted(true);
|
||||
this->switch_.turn_on();
|
||||
EXPECT_FALSE(this->level_());
|
||||
EXPECT_TRUE(this->switch_.state);
|
||||
this->switch_.turn_off();
|
||||
EXPECT_TRUE(this->level_());
|
||||
EXPECT_FALSE(this->switch_.state);
|
||||
}
|
||||
|
||||
TEST_F(BQ25186PgGpoTest, SetupWritesTheRestoredLevel) {
|
||||
this->switch_.set_restore_mode(switch_::SWITCH_ALWAYS_ON);
|
||||
this->hub_.setup();
|
||||
EXPECT_TRUE(this->level_());
|
||||
EXPECT_NE(this->bus_.registers[BQ25186_REG_VBAT_CTRL] & BQ25186_VBAT_CTRL_PG_MODE, 0);
|
||||
EXPECT_TRUE(this->switch_.state);
|
||||
}
|
||||
|
||||
TEST_F(BQ25186PgGpoTest, SetupWritesTheRestoredLevelInverted) {
|
||||
this->bus_.registers[BQ25186_REG_SYS_REG] |= BQ25186_SYS_REG_PG_GPO_LEVEL;
|
||||
this->switch_.set_restore_mode(switch_::SWITCH_ALWAYS_ON);
|
||||
this->switch_.set_inverted(true);
|
||||
this->hub_.setup();
|
||||
EXPECT_FALSE(this->level_());
|
||||
EXPECT_TRUE(this->switch_.state);
|
||||
}
|
||||
|
||||
TEST_F(BQ25186PgGpoTest, SetupWithoutRestoreTakesTheChipLevel) {
|
||||
this->bus_.registers[BQ25186_REG_SYS_REG] |= BQ25186_SYS_REG_PG_GPO_LEVEL;
|
||||
this->hub_.setup();
|
||||
EXPECT_TRUE(this->level_());
|
||||
EXPECT_TRUE(this->switch_.state);
|
||||
}
|
||||
|
||||
TEST_F(BQ25186PgGpoTest, SetupWithoutRestoreTakesTheChipLevelInverted) {
|
||||
this->bus_.registers[BQ25186_REG_SYS_REG] |= BQ25186_SYS_REG_PG_GPO_LEVEL;
|
||||
this->switch_.set_inverted(true);
|
||||
this->hub_.setup();
|
||||
EXPECT_TRUE(this->level_());
|
||||
EXPECT_FALSE(this->switch_.state);
|
||||
}
|
||||
|
||||
} // namespace esphome::bq25186::testing
|
||||
@@ -17,7 +17,7 @@ class TestMicrophone : public Microphone {
|
||||
void start() override {}
|
||||
void stop() override {}
|
||||
|
||||
void publish(std::span<uint8_t> data, const audio::AudioStreamInfo &stream_info, int64_t capture_time_us) {
|
||||
void publish(std::span<const uint8_t> data, const audio::AudioStreamInfo &stream_info, int64_t capture_time_us) {
|
||||
this->publish_audio_(data, stream_info, capture_time_us);
|
||||
}
|
||||
bool has_callbacks() const { return this->has_data_callbacks_(); }
|
||||
@@ -56,24 +56,20 @@ TEST(MicrophoneDataCallbacks, CallbackGetsDataFormatAndCaptureTime) {
|
||||
});
|
||||
EXPECT_TRUE(mic.has_callbacks());
|
||||
|
||||
std::vector<uint8_t> buffer = SAMPLES;
|
||||
mic.publish(buffer, STEREO_48K, 123456);
|
||||
mic.publish(SAMPLES, STEREO_48K, 123456);
|
||||
EXPECT_EQ(data, SAMPLES);
|
||||
EXPECT_EQ(stream_info, STEREO_48K);
|
||||
EXPECT_EQ(capture_time_us, 123456);
|
||||
}
|
||||
|
||||
TEST(MicrophoneDataCallbacks, MuteZeroesTheBufferInPlace) {
|
||||
TEST(MicrophoneDataCallbacks, MutedCallbackGetsZeros) {
|
||||
TestMicrophone mic;
|
||||
std::vector<uint8_t> data;
|
||||
mic.add_data_callback([&](const AudioCapture &capture) { data.assign(capture.data.begin(), capture.data.end()); });
|
||||
|
||||
mic.set_mute_state(true);
|
||||
std::vector<uint8_t> buffer = SAMPLES;
|
||||
mic.publish(buffer, STEREO_48K, 0);
|
||||
const std::vector<uint8_t> zeros(SAMPLES.size(), 0);
|
||||
EXPECT_EQ(data, zeros);
|
||||
EXPECT_EQ(buffer, zeros);
|
||||
mic.publish(SAMPLES, STEREO_48K, 0);
|
||||
EXPECT_EQ(data, std::vector<uint8_t>(SAMPLES.size(), 0));
|
||||
}
|
||||
|
||||
TEST(MicrophoneDataCallbacks, DataTriggerPassesACopy) {
|
||||
@@ -119,17 +115,4 @@ TEST(MicrophoneDataCallbacks, DeprecatedCallUsesMicrophoneFormat) {
|
||||
EXPECT_EQ(capture_time_us, 0);
|
||||
}
|
||||
|
||||
// Remove in 2027.5.0 with the deprecated DataCallbackManager::call(data)
|
||||
TEST(MicrophoneDataCallbacks, DeprecatedCallDeliversZerosWhileMuted) {
|
||||
TestMicrophone mic;
|
||||
std::vector<uint8_t> data;
|
||||
mic.add_data_callback([&](const AudioCapture &capture) { data.assign(capture.data.begin(), capture.data.end()); });
|
||||
|
||||
mic.set_mute_state(true);
|
||||
const std::vector<uint8_t> buffer = SAMPLES;
|
||||
mic.publish_deprecated(buffer);
|
||||
EXPECT_EQ(data, std::vector<uint8_t>(SAMPLES.size(), 0));
|
||||
EXPECT_EQ(buffer, SAMPLES);
|
||||
}
|
||||
|
||||
} // namespace esphome::microphone::testing
|
||||
@@ -1,11 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
# filter.cpp only compiles with the define the real to_code emits for configured filters
|
||||
cg.add_define("USE_TEXT_SENSOR_FILTER")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -30,29 +30,6 @@ text_sensor:
|
||||
- substitute:
|
||||
- BAR -> baz
|
||||
|
||||
- platform: template
|
||||
name: "Test Throttle"
|
||||
id: test_throttle
|
||||
filters:
|
||||
- throttle: 1s
|
||||
|
||||
- platform: template
|
||||
name: "Test Throttle With Priority"
|
||||
id: test_throttle_with_priority
|
||||
filters:
|
||||
- timeout: 1s
|
||||
- throttle_with_priority:
|
||||
timeout: 1s
|
||||
value: NA
|
||||
|
||||
- platform: template
|
||||
name: "Test Timeout Value"
|
||||
id: test_timeout_value
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 30s
|
||||
value: Offline
|
||||
|
||||
- platform: template
|
||||
name: "Test Map Single"
|
||||
id: test_map_single
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/text_sensor/filter.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::text_sensor::testing {
|
||||
|
||||
class TextFilterTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->sensor_.add_on_state_callback([this](const std::string &state) { this->seen_.push_back(state); });
|
||||
}
|
||||
void publish_at_(uint32_t now, const char *value) {
|
||||
LoopBlockingGuard guard{nullptr, nullptr, now};
|
||||
this->sensor_.publish_state(value);
|
||||
}
|
||||
static void loop_at(Component &filter, uint32_t now) {
|
||||
LoopBlockingGuard guard{nullptr, nullptr, now};
|
||||
filter.loop();
|
||||
}
|
||||
|
||||
TextSensor sensor_;
|
||||
std::vector<std::string> seen_;
|
||||
};
|
||||
|
||||
static constexpr const char *PRIORITIZED[] = {"NA", nullptr};
|
||||
|
||||
TEST_F(TextFilterTest, ThrottleDropsValuesInsideThePeriod) {
|
||||
ThrottleFilter throttle(1000);
|
||||
this->sensor_.add_filter(&throttle);
|
||||
this->publish_at_(1000, "a");
|
||||
this->publish_at_(1500, "b");
|
||||
this->publish_at_(2000, "c");
|
||||
EXPECT_EQ(this->seen_, (std::vector<std::string>{"a", "c"}));
|
||||
}
|
||||
|
||||
TEST_F(TextFilterTest, PrioritizedValuePassesAtOnce) {
|
||||
ThrottleWithPriorityFilter throttle(1000, PRIORITIZED);
|
||||
this->sensor_.add_filter(&throttle);
|
||||
this->publish_at_(1000, "a");
|
||||
this->publish_at_(1100, "NA");
|
||||
this->publish_at_(1200, "b");
|
||||
this->publish_at_(2100, "c");
|
||||
EXPECT_EQ(this->seen_, (std::vector<std::string>{"a", "NA", "c"}));
|
||||
}
|
||||
|
||||
TEST_F(TextFilterTest, TimeoutRearmsOnEachValueAndFiresOnce) {
|
||||
TimeoutFilterLast timeout(1000);
|
||||
this->sensor_.add_filter(&timeout);
|
||||
this->publish_at_(1000, "a");
|
||||
this->publish_at_(1800, "b");
|
||||
loop_at(timeout, 2500);
|
||||
EXPECT_EQ(this->seen_, (std::vector<std::string>{"a", "b"}));
|
||||
EXPECT_FALSE(timeout.is_idle());
|
||||
loop_at(timeout, 2800);
|
||||
EXPECT_EQ(this->seen_, (std::vector<std::string>{"a", "b", "b"}));
|
||||
EXPECT_TRUE(timeout.is_idle());
|
||||
}
|
||||
|
||||
TEST_F(TextFilterTest, TimeoutSendsTheConfiguredValue) {
|
||||
TimeoutFilterConfigured timeout(1000, "Offline");
|
||||
this->sensor_.add_filter(&timeout);
|
||||
this->publish_at_(1000, "a");
|
||||
loop_at(timeout, 2000);
|
||||
EXPECT_EQ(this->seen_, (std::vector<std::string>{"a", "Offline"}));
|
||||
}
|
||||
|
||||
// Feeds the same values to the timeout + throttle chain and to the merged filter
|
||||
static std::vector<std::string> run_sequence(std::initializer_list<Filter *> filters, Component &timeout) {
|
||||
TextSensor sensor;
|
||||
std::vector<std::string> seen;
|
||||
sensor.add_on_state_callback([&seen](const std::string &state) { seen.push_back(state); });
|
||||
sensor.add_filters(filters);
|
||||
const auto publish_at = [&sensor](uint32_t now, const char *value) {
|
||||
LoopBlockingGuard guard{nullptr, nullptr, now};
|
||||
sensor.publish_state(value);
|
||||
};
|
||||
const auto loop_at = [&timeout](uint32_t now) {
|
||||
LoopBlockingGuard guard{nullptr, nullptr, now};
|
||||
timeout.loop();
|
||||
};
|
||||
publish_at(1000, "a");
|
||||
publish_at(1200, "b"); // held by the throttle, sent once updates pause
|
||||
loop_at(2200);
|
||||
publish_at(2300, "c");
|
||||
publish_at(2400, "NA"); // a priority value passes at once
|
||||
publish_at(2500, "d");
|
||||
loop_at(3500);
|
||||
return seen;
|
||||
}
|
||||
|
||||
TEST(TextFilterChain, CombinedTimeoutThrottleMatchesTheChain) {
|
||||
TimeoutFilterLast timeout(1000);
|
||||
ThrottleWithPriorityFilter throttle(1000, PRIORITIZED);
|
||||
const auto chain = run_sequence({&timeout, &throttle}, timeout);
|
||||
TimeoutThrottleFilter combined(1000, PRIORITIZED);
|
||||
EXPECT_EQ(run_sequence({&combined}, combined), chain);
|
||||
EXPECT_EQ(chain, (std::vector<std::string>{"a", "b", "NA", "d"}));
|
||||
}
|
||||
|
||||
TEST_F(TextFilterTest, CombinedWithoutPriorityIsAPlainThrottle) {
|
||||
TimeoutThrottleFilter combined(1000, nullptr);
|
||||
this->sensor_.add_filter(&combined);
|
||||
this->publish_at_(1000, "a");
|
||||
this->publish_at_(1300, "NA");
|
||||
loop_at(combined, 2300);
|
||||
EXPECT_EQ(this->seen_, (std::vector<std::string>{"a", "NA"}));
|
||||
}
|
||||
|
||||
} // namespace esphome::text_sensor::testing
|
||||
@@ -1,28 +0,0 @@
|
||||
"""Tests for the esp32 framework ``components:`` schema."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import esp32
|
||||
import esphome.config_validation as cv
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source",
|
||||
[
|
||||
"https://github.com/owner/component.git",
|
||||
"git@github.com:owner/component.git",
|
||||
],
|
||||
)
|
||||
def test_component_source_accepts_repository_url(source: str) -> None:
|
||||
config = esp32.FRAMEWORK_SCHEMA(
|
||||
{"components": [{"name": "owner/component", "source": source}]}
|
||||
)
|
||||
assert config["components"][0]["source"] == source
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", [1.0, ""])
|
||||
def test_component_source_rejects_invalid(source: object) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
esp32.FRAMEWORK_SCHEMA(
|
||||
{"components": [{"name": "owner/component", "source": source}]}
|
||||
)
|
||||
@@ -1711,6 +1711,14 @@ _SELECT_APPLY = entity_helpers.ApplyEntity(
|
||||
"after",
|
||||
"::hub->set_mode(index, options_);\nentity->publish_state(index);",
|
||||
),
|
||||
(
|
||||
"set_mode(option)",
|
||||
"on_success",
|
||||
(
|
||||
"const char *option = entity->option_at(index);\n"
|
||||
"if (::hub->set_mode(option)) { entity->publish_state(index); }"
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_sub_entities_apply_locals(
|
||||
@@ -1883,6 +1891,35 @@ async def test_sub_entities_apply_momentary(
|
||||
assert apply_functions[0][2] == body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("parent", "apply", "body"),
|
||||
[
|
||||
(
|
||||
_HUB,
|
||||
"write_level(state)",
|
||||
"if (::hub->write_level(state)) { entity->publish_state(state); }",
|
||||
),
|
||||
(
|
||||
None,
|
||||
"chip::write_level(state)",
|
||||
"if (chip::write_level(state)) { entity->publish_state(state); }",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_sub_entities_apply_on_success(
|
||||
setup_test_environment: list[str],
|
||||
apply_functions: list[tuple[str, str, str]],
|
||||
parent: MockObj | None,
|
||||
apply: str,
|
||||
body: str,
|
||||
) -> None:
|
||||
"""publish="on_success" publishes only when the hub call returns true."""
|
||||
sub = _apply_sub([], {"k": _apply_conf("k_sw")}, parent=parent)
|
||||
await sub("k", None, apply=apply, publish="on_success")
|
||||
assert apply_functions[0][2] == body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sub_entities_apply_without_parent_needs_namespaced_call(
|
||||
setup_test_environment: list[str],
|
||||
|
||||
@@ -3121,21 +3121,9 @@ def test_git_ref_valid() -> None:
|
||||
assert cv.git_ref("v1.2.3") == "v1.2.3"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ref", ["main", "release/v6.0", "feat+x", "a1b2c3d4"])
|
||||
def test_git_ref_valid_chars(ref: str) -> None:
|
||||
assert cv.git_ref(ref) == ref
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ref", ["main hi there", "a:b", "x~1", "", "main\n"])
|
||||
def test_git_ref_invalid(ref: str) -> None:
|
||||
def test_git_ref_invalid() -> None:
|
||||
with pytest.raises(Invalid, match="Not a valid git ref"):
|
||||
cv.git_ref(ref)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ref", [1.0, 2025, None])
|
||||
def test_git_ref_not_string(ref: object) -> None:
|
||||
with pytest.raises(Invalid, match="Must be string"):
|
||||
cv.git_ref(ref)
|
||||
cv.git_ref("!!!")
|
||||
|
||||
|
||||
def test_source_refresh_always() -> None:
|
||||
@@ -3303,34 +3291,6 @@ def test_validate_source_shorthand_github_pr(setup_core: Path) -> None:
|
||||
assert result[CONF_REF] == "pull/1234/head"
|
||||
|
||||
|
||||
def test_validate_source_shorthand_github_ref_with_slash(setup_core: Path) -> None:
|
||||
result = cv.validate_source_shorthand("github://user/repo@release/v1")
|
||||
assert result[CONF_REF] == "release/v1"
|
||||
|
||||
|
||||
def test_validate_source_shorthand_github_ref_keeps_all_chars(
|
||||
setup_core: Path,
|
||||
) -> None:
|
||||
result = cv.validate_source_shorthand("github://user/repo@feat+x")
|
||||
assert result[CONF_REF] == "feat+x"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
[
|
||||
"github://esphome/esphome/esphome/components@dev",
|
||||
"github://user/repo/",
|
||||
"github://user/repo@main hi",
|
||||
"github://pr#1234abc",
|
||||
],
|
||||
)
|
||||
def test_validate_source_shorthand_rejects_trailing_text(
|
||||
setup_core: Path, value: str
|
||||
) -> None:
|
||||
with pytest.raises(Invalid, match="not a file system path"):
|
||||
cv.validate_source_shorthand(value)
|
||||
|
||||
|
||||
def test_validate_source_shorthand_invalid(setup_core: Path) -> None:
|
||||
with pytest.raises(Invalid, match="not a file system path"):
|
||||
cv.validate_source_shorthand("notvalid")
|
||||
|
||||
@@ -3332,26 +3332,6 @@ def test_git_file_from_shorthand_without_ref_or_query() -> None:
|
||||
assert gf == git.GitFile("codeberg.org", "owner", "repo", "file.yaml")
|
||||
|
||||
|
||||
def test_git_file_from_shorthand_keeps_whole_ref() -> None:
|
||||
gf = git.GitFile.from_shorthand("github://owner/repo/file.yaml@feat+x?full_config")
|
||||
|
||||
assert gf.ref == "feat+x"
|
||||
assert gf.query == "full_config"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"shorthand",
|
||||
[
|
||||
"github://owner/repo/file.yaml@main hi",
|
||||
"github://owner/repo/my file.yaml",
|
||||
"github://owner/repo/file.yaml?full config",
|
||||
],
|
||||
)
|
||||
def test_git_file_from_shorthand_rejects_trailing_text(shorthand: str) -> None:
|
||||
with pytest.raises(ValueError, match="not in expected"):
|
||||
git.GitFile.from_shorthand(shorthand)
|
||||
|
||||
|
||||
def test_git_file_from_shorthand_requires_string() -> None:
|
||||
with pytest.raises(ValueError, match="must be a string"):
|
||||
git.GitFile.from_shorthand(123)
|
||||
|
||||
@@ -26,7 +26,6 @@ from esphome.loader import (
|
||||
clear_component_meta_finders,
|
||||
get_alias_metadata,
|
||||
get_component,
|
||||
get_platform,
|
||||
install_meta_finder,
|
||||
)
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
@@ -810,44 +809,6 @@ def test_lookup_module_import_error_missing_module_returns_none(
|
||||
assert "definitely_missing_component_xyz" not in _COMPONENT_CACHE
|
||||
|
||||
|
||||
def test_lookup_module_missing_platform_returns_none(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A platform file that does not exist is "not found", not an error."""
|
||||
with caplog.at_level(logging.INFO, logger="esphome.loader"):
|
||||
assert get_platform("definitely_missing_platform_xyz", "template") is None
|
||||
assert "Unable to import component template.definitely_missing_platform_xyz" in (
|
||||
caplog.text
|
||||
)
|
||||
assert not [r for r in caplog.records if r.levelno >= logging.ERROR]
|
||||
|
||||
|
||||
def test_lookup_module_missing_dependency_is_logged_as_error(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A component that exists but imports a missing package reports that
|
||||
package as an error, instead of looking like a missing component."""
|
||||
comp = tmp_path / "depfailcomp"
|
||||
comp.mkdir()
|
||||
(comp / "__init__.py").write_text("import definitely_missing_package_xyz\n")
|
||||
saved = list(sys.meta_path)
|
||||
try:
|
||||
install_meta_finder(tmp_path)
|
||||
with caplog.at_level(logging.INFO, logger="esphome.loader"):
|
||||
assert get_component("depfailcomp") is None
|
||||
finally:
|
||||
sys.meta_path[:] = saved
|
||||
for name in [
|
||||
m for m in sys.modules if m.startswith("esphome.components.depfailcomp")
|
||||
]:
|
||||
del sys.modules[name]
|
||||
errors = [r for r in caplog.records if r.levelno >= logging.ERROR]
|
||||
assert len(errors) == 1
|
||||
assert errors[0].getMessage() == "Unable to import component depfailcomp:"
|
||||
assert errors[0].exc_info is not None
|
||||
assert errors[0].exc_info[1].name == "definitely_missing_package_xyz"
|
||||
|
||||
|
||||
def test_lookup_module_missing_module_raises_when_requested() -> None:
|
||||
"""With ``exception=True`` the ImportError propagates."""
|
||||
with pytest.raises(ImportError):
|
||||
|
||||
Reference in new issue
Block a user