Compare commits

..
Author SHA1 Message Date
J. Nick Koston f7b42f33a1 Merge remote-tracking branch 'origin/dev' into HEAD 2026-11-14 08:39:12 -06:00
J. Nick Koston d48ea6cd45 [binary_sensor] Write delayed_on and delayed_off as two plain statements 2026-11-11 19:09:33 -06:00
J. Nick Koston f60fe6113a Merge remote-tracking branch 'upstream/dev' into apply-filter-binary-delay-onoff
# Conflicts:
#	esphome/components/binary_sensor/__init__.py
#	esphome/components/binary_sensor/filter.cpp
#	esphome/components/binary_sensor/filter.h
2026-11-11 19:07:44 -06:00
J. Nick Koston 2169df4aa9 Merge remote-tracking branch 'origin/apply-filter-binary-delay2' into apply-filter-binary-delay-onoff 2026-11-11 18:15:30 -06:00
J. Nick Koston d53740333d Merge remote-tracking branch 'upstream/dev' into apply-filter-binary-delay2 2026-11-11 18:14:51 -06:00
J. Nick Koston b3a9677e04 Merge remote-tracking branch 'origin/apply-filter-binary-delay2' into apply-filter-binary-delay-onoff
# Conflicts:
#	esphome/components/binary_sensor/__init__.py
#	esphome/components/binary_sensor/filter.cpp
#	esphome/components/binary_sensor/filter.h
#	tests/components/binary_sensor/apply_filter_test.cpp
2026-11-11 18:03:03 -06:00
J. Nick Koston 85a54a85a4 Pass the schema of each delayed_on_off form to APPLY_FILTER.build 2026-11-11 18:26:55 -05:00
J. Nick Koston 10890c05db Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2
The delay statements name the plain filter parameter.
2026-11-11 18:26:55 -05:00
J. Nick Koston 6c70240bcc Keep a delayed_on_off lambda per edge and evaluate only the edge's delay
A single lambda for both edges was two lambdas before, each with its own
static state, so it becomes one function per edge again. The generated
code picks the delay for the edge, so only that value is evaluated; the
helper that took both delays goes.
2026-11-11 18:26:55 -05:00
J. Nick Koston 8f920c017d Generate delayed_on_off as one call to a C++ helper
delayed_on_off_output picks the delay for the edge, so the generated
statements are plain strings with one placeholder per value.
2026-11-11 18:26:55 -05:00
J. Nick Koston c4a6af22eb Make delay_output a codegen-only free function and leave the delay classes alone
delay_output takes the filter instead of being a member of the public
Filter base, and DelayedOnFilter and DelayedOffFilter keep their own
code.
2026-11-11 18:26:55 -05:00
J. Nick Koston 2c5dc323b7 Emit a single delayed_on_off delay once and drop DelayedOnOffFilter
The single value form is its own statement, so a lambda delay is one
function. DelayedOnOffFilter only existed for codegen; DelayedOnFilter
and DelayedOffFilter schedule through Filter::delay_output, so all
delay filters share one scheduler callback type.
2026-11-11 18:26:55 -05:00
J. Nick Koston 61d68292b0 Generate the binary sensor delayed_on_off filter as an apply filter
The generated function picks the delay and schedules the output through
Filter::delay_output, so the filter object drops its two TemplatableFn
and equal filters share one function. A single delay value is used for
both edges.
2026-11-11 18:26:55 -05:00
J. Nick Koston 56870c31cc Merge remote-tracking branch 'origin/apply-filter-binary-delay2' into apply-filter-binary-delay-onoff 2026-11-11 13:02:37 -06:00
J. Nick Koston a8f47a1457 Merge remote-tracking branch 'origin/apply-filter-helper2' into apply-filter-binary-delay2 2026-11-11 13:02:33 -06:00
J. Nick Koston 7a51ca87b4 Merge remote-tracking branch 'origin/dev' into apply-filter-helper2 2026-11-11 13:01:08 -06:00
J. Nick Koston 9125361dac Share the delay schema between the delay filters 2026-11-10 11:04:15 -08:00
J. Nick Koston 70cc52bc71 Merge remote-tracking branch 'origin/apply-filter-binary-delay2' into st-apply-filter-binary-delay-onoff 2026-11-10 11:03:13 -08:00
J. Nick Koston bb3e9fab4b Pass the schema of each delayed_on_off form to APPLY_FILTER.build 2026-11-10 11:02:37 -08:00
J. Nick Koston 8a533631c1 Merge remote-tracking branch 'origin/apply-filter-helper2' into st-apply-filter-binary-delay2 2026-11-10 11:00:11 -08:00
J. Nick Koston 6521dd8269 Require the schema in ApplyFilterDomain.build so a misspelled key always fails 2026-11-10 10:58:38 -08:00
J. Nick Koston bdf4643b97 Sync the shared function helper: accept Statement lines in the body 2026-11-09 21:34:14 -08:00
J. Nick Koston bb9ec63e6b Reject a filter with no statements 2026-11-09 21:23:58 -08:00
J. Nick Koston aeb77b1e6b Keep the value range function name; sync the shared function helper; trim docstrings and tests 2026-11-09 21:08:48 -08:00
J. Nick Koston e627f13b42 Merge remote-tracking branch 'upstream/dev' into apply-filter-helper2
# Conflicts:
#	esphome/automation.py
2026-11-09 20:50:25 -08:00
J. Nick Koston 057ec35b1e Merge branch 'apply-filter-binary-delay2' into apply-filter-binary-delay-onoff 2026-11-04 18:35:36 -06:00
J. Nick Koston 64fa737ae3 Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2 2026-11-04 18:35:36 -06:00
J. Nick Koston c4133a1af5 Merge remote-tracking branch 'origin/dev' into apply-filter-helper2
# Conflicts:
#	tests/unit_tests/test_cpp_generator.py
2026-11-04 18:33:16 -06:00
J. Nick Koston c2f651b803 Test that build() with a schema accepts valid keys 2026-11-03 19:43:43 -06:00
J. Nick Koston c7472e9068 Merge remote-tracking branch 'origin/dev' into apply-filter-helper2 2026-11-03 19:30:00 -06:00
J. Nick Koston 2fdd4434dd Reject a skipped last apply filter statement and let build check schema keys 2026-11-03 19:09:25 -06:00
J. Nick Koston acc7fd2143 [core] Adapt apply filters to the dev apply function changes
Filter functions keep the parameters the filter class passes, so they are
emitted directly instead of through _apply_function, which now turns value
parameters into const references. The filter body renders through
_render_call, which replaced _render_values.
2026-11-03 18:52:25 -06:00
J. Nick Koston fc9743e5c7 Merge remote-tracking branch 'origin/dev' into apply-filter-helper2
# Conflicts:
#	esphome/automation.py
2026-11-03 18:50:21 -06:00
J. Nick Koston c374138410 Merge branch 'apply-filter-binary-delay2' into apply-filter-binary-delay-onoff 2026-11-02 20:42:11 -06:00
J. Nick Koston 924b1f22ae Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2 2026-11-02 20:42:04 -06:00
J. Nick Koston a26ab932e2 Merge remote-tracking branch 'origin/dev' into apply-filter-helper2
# Conflicts:
#	tests/unit_tests/test_automation.py
2026-11-02 20:41:39 -06:00
J. Nick Koston 8e4ca88f4a Merge apply-filter-binary-delay2 2026-11-02 09:36:56 -06:00
J. Nick Koston da88755288 Merge apply-filter-helper2 2026-11-02 09:36:44 -06:00
J. Nick Koston 3fd62a666c Merge dev 2026-11-02 09:36:15 -06:00
J. Nick Koston f34719cec5 Merge dev 2026-11-02 09:34:42 -06:00
J. Nick Koston d338b3d1ee Merge apply-filter-binary-delay2 2026-11-02 09:33:29 -06:00
J. Nick Koston 3ccba4c31c Merge remote-tracking branch 'origin/apply-filter-helper2' into apply-filter-binary-delay2 2026-10-30 20:11:49 -10:00
J. Nick Koston 3ebb022f55 Merge remote-tracking branch 'origin/dev' into apply-filter-helper2
# Conflicts:
#	esphome/automation.py
#	tests/unit_tests/test_automation.py
2026-10-30 20:11:25 -10:00
J. Nick Koston 8e66eb360b Merge branch 'apply-filter-binary-delay2' into apply-filter-binary-delay-onoff 2026-10-28 18:45:21 -10:00
J. Nick Koston 7b25caf9d1 Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2 2026-10-28 18:45:20 -10:00
J. Nick Koston e732191329 Merge remote-tracking branch 'origin/codegen-shared-static-function' into apply-filter-helper2 2026-10-28 18:45:19 -10:00
J. Nick Koston 2be6a24507 Record apply functions where shared_static_function emits them 2026-10-28 18:43:42 -10:00
J. Nick Koston 6b8bce8385 Merge branch 'apply-filter-binary-delay2' into apply-filter-binary-delay-onoff
# Conflicts:
#	esphome/components/binary_sensor/__init__.py
2026-10-28 18:35:21 -10:00
J. Nick Koston b6e2e8ae1d Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2
The delay statements name the plain filter parameter.
2026-10-28 18:35:00 -10:00
J. Nick Koston 136790e907 Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2 2026-10-28 18:34:44 -10:00
J. Nick Koston 5a838cf0e6 Name the binary sensor apply filter's parameter plain filter
The generated function only holds statements from code generation; user
lambda code is always in functions of its own.
2026-10-28 18:34:13 -10:00
J. Nick Koston d3963a82db Merge remote-tracking branch 'origin/codegen-shared-static-function' into apply-filter-helper2
# Conflicts:
#	esphome/automation.py
2026-10-28 18:29:57 -10:00
J. Nick Koston 206ab92115 Give unshared apply functions config-id-safe names too
apply_static_function always goes through cg.shared_static_function, so
an unshared function also gets a name no config id uses.
2026-10-28 18:29:30 -10:00
J. Nick Koston d89c77ced5 Merge branch 'apply-filter-binary-delay2' into apply-filter-binary-delay-onoff 2026-10-28 18:23:00 -10:00
J. Nick Koston 2ee7ff993b Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2 2026-10-28 18:22:59 -10:00
J. Nick Koston 2c7748606e Merge remote-tracking branch 'origin/codegen-shared-static-function' into apply-filter-helper2 2026-10-28 18:22:56 -10:00
J. Nick Koston 907b3c6d6a [core] Key shared static functions on the rendered body text 2026-10-28 18:06:43 -10:00
J. Nick Koston 651d8261a4 Merge branch 'apply-filter-binary-delay2' into apply-filter-binary-delay-onoff
delayed_on and delayed_off go back to picking the edge in the generated
code, so a lambda delay is only evaluated on its edge; their inline
helpers go.
2026-10-28 18:04:37 -10:00
J. Nick Koston 35b0f4b814 Keep a delayed_on_off lambda per edge and evaluate only the edge's delay
A single lambda for both edges was two lambdas before, each with its own
static state, so it becomes one function per edge again. The generated
code picks the delay for the edge, so only that value is evaluated; the
helper that took both delays goes.
2026-10-28 18:03:56 -10:00
J. Nick Koston 7db36bb9f1 Merge branch 'apply-filter-binary-delay2' into apply-filter-binary-delay-onoff
Resolved onto the C++ helpers: delayed_on and delayed_off call
delayed_on_output and delayed_off_output, so every delay statement is a
plain string with one placeholder per value.
2026-10-28 18:00:54 -10:00
J. Nick Koston de8da2ccf1 Generate delayed_on_off as one call to a C++ helper
delayed_on_off_output picks the delay for the edge, so the generated
statements are plain strings with one placeholder per value.
2026-10-28 18:00:31 -10:00
J. Nick Koston 5d4bb552ef Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2 2026-10-28 18:00:23 -10:00
J. Nick Koston 7f7e4af3ea Name the binary sensor apply filter's filter parameter in one constant 2026-10-28 18:00:05 -10:00
J. Nick Koston 11e1ef3941 Merge branch 'apply-filter-binary-delay2' into apply-filter-binary-delay-onoff 2026-10-28 17:58:06 -10:00
J. Nick Koston 5e8446dfc6 Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2 2026-10-28 17:58:06 -10:00
J. Nick Koston 62d074aae6 Merge branch 'filter-behaviour-tests' into apply-filter-helper2 2026-10-28 17:58:04 -10:00
J. Nick Koston 250ed963cc Merge commit 'e6bebc298f7ddf77c4d7d6322f79b22a08170ba5' into filter-behaviour-tests 2026-10-28 17:58:04 -10:00
J. Nick Koston f0c4ae5fce Merge branch 'apply-filter-binary-delay2' into apply-filter-binary-delay-onoff
Resolved onto the free delay_output: cancel_and_pass becomes a free
codegen-only function too, the delay ApplyCalls are module constants,
the delay_output tests move to apply_filter_test.cpp, and the duplicate
pending output test goes.
2026-10-28 17:31:14 -10:00
J. Nick Koston 56a9cc27a0 Make delay_output a codegen-only free function and leave the delay classes alone
delay_output takes the filter instead of being a member of the public
Filter base, and DelayedOnFilter and DelayedOffFilter keep their own
code.
2026-10-28 17:30:09 -10:00
J. Nick Koston dfec0df2a5 Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2 2026-10-28 17:29:52 -10:00
J. Nick Koston ed076e9210 Share apply filter functions through cg.shared_static_function
The private cache goes; _apply_function passes share on to
apply_static_function. ApplyFilterDomain.new shares only when asked,
value lambda functions are named by apply_static_function, statements
become ApplyCall in one place and render with _render_values, and one
helper checks a call's keys against the schema.
2026-10-28 17:27:28 -10:00
J. Nick Koston 9aa0a835c4 Merge branch 'codegen-shared-static-function' into apply-filter-helper2
# Conflicts:
#	tests/unit_tests/test_automation.py
2026-10-28 17:26:36 -10:00
J. Nick Koston 8c9d117be5 Merge branch 'filter-behaviour-tests' into apply-filter-helper2 2026-10-28 17:26:24 -10:00
J. Nick Koston 1c33d6b960 Pin value lambdas reading an id named x, and keep only the filters the apply stack changes
A sensor offset lambda and a binary delayed_on_off lambda read an entity
with id x. The round, round_to_multiple_of and calibrate_polynomial cases
go, since no change touches those filters; calibrate_linear exact stays
as a control. Each test reads the log lines once.
2026-10-28 17:26:20 -10:00
J. Nick Koston f4d3dfa01c Keep a 0 ms delay on the scheduler for the delay filters
delay_output schedules even a 0 ms delay again, so edges within one loop
pass still coalesce. The undelayed edge of delayed_on and delayed_off
calls cancel_and_pass, which drops the pending output and passes the
value on, as the old classes did.
2026-10-28 17:20:20 -10:00
J. Nick Koston beb66e71bf Generate the delayed_on and delayed_off filters as apply filters
Filter::delay_output with a delay of 0 drops any pending output and
passes the value on at once, which is what delayed_on and delayed_off do
on their undelayed edge, so each is one call with the delay picked by the
value. DelayedOnFilter and DelayedOffFilter only existed for codegen.
A delayed_on_off delay of 0 now passes each value at once instead of one
scheduler pass later.
2026-10-28 17:14:45 -10:00
J. Nick Koston 10f582067c Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2 2026-10-28 16:47:09 -10:00
J. Nick Koston be88427d00 Merge branch 'filter-behaviour-tests' into apply-filter-helper2 2026-10-28 16:46:08 -10:00
J. Nick Koston fbeb90208b Explain why the b_constant to_ntc_resistance case stays NaN 2026-10-28 16:46:04 -10:00
J. Nick Koston 5836fd1583 Note in the description why removing the codegen-only classes is not breaking 2026-10-28 16:37:04 -10:00
J. Nick Koston b593034a2f Make shared static function names unique and add share to apply_static_function 2026-10-28 16:34:16 -10:00
J. Nick Koston bb1713e733 Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2 2026-10-28 16:27:36 -10:00
J. Nick Koston a644d8d9a0 Check the timing of the delayed_on_off filter that reads an id x 2026-10-28 16:27:31 -10:00
J. Nick Koston f2651d794d Merge branch 'filter-behaviour-tests' into apply-filter-helper2 2026-10-28 16:27:16 -10:00
J. Nick Koston 2ed6453467 Check when each delayed binary sensor output lands
Each output must come its delay after the source edge it follows, within
a margin for scheduler jitter, so a lost delay fails the test.
2026-10-28 16:27:13 -10:00
J. Nick Koston 9f6b78930b [core] Add cg.shared_static_function to emit one function per distinct body 2026-10-28 16:25:42 -10:00
J. Nick Koston 55c21d180c Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2 2026-10-28 16:22:09 -10:00
J. Nick Koston cc7a9db510 Merge branch 'filter-behaviour-tests' into apply-filter-helper2 2026-10-28 16:21:59 -10:00
J. Nick Koston a20755a857 Fix the fixture indentation for yamllint 2026-10-28 16:21:57 -10:00
J. Nick Koston bc3accb433 Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2 2026-10-28 16:21:01 -10:00
J. Nick Koston 5859db98cb Parenthesize the multi-line strings in the shared value test 2026-10-28 16:20:54 -10:00
J. Nick Koston d99dcf3af6 Emit a single delayed_on_off delay once and drop DelayedOnOffFilter
The single value form is its own statement, so a lambda delay is one
function. DelayedOnOffFilter only existed for codegen; DelayedOnFilter
and DelayedOffFilter schedule through Filter::delay_output, so all
delay filters share one scheduler callback type.
2026-10-28 16:03:17 -10:00
J. Nick Koston e295823b74 Merge branch 'apply-filter-helper2' into apply-filter-binary-delay2 2026-10-28 16:01:39 -10:00
J. Nick Koston 9a8fbf2e2b Let an apply filter builder pick its statements, and keep shared functions in a dataclass
ApplyFilterDomain.build renders the statements over a config, for a
builder that picks them by the shape of its config; prepare is dropped.
The shared functions live in a CORE.data dataclass. Each domain's
APPLY_FILTER sits under its ApplyFilter, and every domain's test checks
the filter has no bytes beyond its base.
2026-10-28 16:01:34 -10:00
J. Nick Koston c0ff8d7e98 Pin text_sensor lambda filters that replace and drop a value
Also mark the to_ntc_resistance b_constant NaN outputs as a known bug.
2026-10-28 16:00:42 -10:00
J. Nick Koston 15aaaae719 Merge branch 'filter-behaviour-tests' into apply-filter-helper2 2026-10-28 16:00:42 -10:00
J. Nick Koston 88eaaa86c8 Give each apply filter value lambda its own function
Inlined into the filter's function, a value lambda saw the function's x
parameter, so an id named x, such as id(x).state, no longer named that
entity and failed to compile. Each value lambda is now a function
without parameters, called from the filter's function, as it was its
own lambda before. The integration tests cover an id named x in a
sensor offset lambda and a binary sensor delayed_on_off lambda.
2026-10-28 15:59:06 -10:00
J. Nick Koston a951a26225 Generate the binary sensor delayed_on_off filter as an apply filter
The generated function picks the delay and schedules the output through
Filter::delay_output, so the filter object drops its two TemplatableFn
and equal filters share one function. A single delay value is used for
both edges.
2026-10-28 14:44:35 -10:00
J. Nick Koston aac2f28df2 Let an apply filter map its config before rendering
prepare turns a shorthand value into the mapping the statements read.
2026-10-28 14:33:08 -10:00
J. Nick Koston 5daa9693d0 Add an apply filter for sensor, binary_sensor and text_sensor
ApplyFilter<Fn> in each domain is a filter whose work and constants are
one generated function, so the object is only the Filter base; the
binary sensor one passes the filter to the function so it can schedule
output. ApplyFilterDomain declares a domain's filters: register() builds
the function from ApplyCall statements, with constants baked in and
value lambdas called inline, and new() takes the statements directly.
Identical functions without user code are emitted once, so equal
constant filters share one class. LambdaExpression.body_lines gives a
lambda's statements with their #line marks for a generated function.
2026-10-28 14:31:25 -10:00
J. Nick Koston 1e6a3c55b6 Pin the output of the stateless sensor filters and the binary sensor delay filters
Integration tests run offset, multiply, clamp, NTC, calibrate_linear,
calibrate_polynomial, lambda and round filters over finite, NaN and
infinite inputs, and the binary sensor delayed_on_off, delayed_on,
delayed_off and lambda filters over one timed input sequence.
2026-10-28 13:58:08 -10:00
28 changed files with 120 additions and 500 deletions

No files matched your search

+1 -1
View File
@@ -741,7 +741,7 @@ esphome/components/web_server_idf/* @dentra
esphome/components/weikai/* @DrCoolZic
esphome/components/weikai_i2c/* @DrCoolZic
esphome/components/weikai_spi/* @DrCoolZic
esphome/components/whirlpool/* @arpena @glmnet @Rodrigoah
esphome/components/whirlpool/* @arpena @glmnet
esphome/components/whynter/* @aeonsablaze
esphome/components/wiegand/* @ssieb
esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard
+13 -25
View File
@@ -147,8 +147,6 @@ MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent")
# Filters
Filter = binary_sensor_ns.class_("Filter")
TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter)
DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter)
DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter)
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter)
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter)
@@ -189,11 +187,10 @@ async def timeout_filter_to_code(config, filter_id):
return var
_DELAY = ((automation.WHOLE_CONFIG, cg.uint32),)
_DELAY_EITHER = automation.ApplyCall(
"return binary_sensor::delay_output(filter, {}, x);",
((automation.WHOLE_CONFIG, cg.uint32),),
"return binary_sensor::delay_output(filter, {}, x);", _DELAY
)
# The delay for the edge is picked in the generated code, so only that value is evaluated
_DELAY_SCHEMA = cv.templatable(cv.positive_time_period_milliseconds)
_DELAY_ON_OFF_SCHEMA = cv.Schema(
{
@@ -201,10 +198,19 @@ _DELAY_ON_OFF_SCHEMA = cv.Schema(
cv.Required(CONF_TIME_OFF): _DELAY_SCHEMA,
}
)
# The delay for the edge is picked in the generated code, so only that value is evaluated
_DELAY_ON_OFF = automation.ApplyCall(
"return binary_sensor::delay_output(filter, x ? {} : {}, x);",
((CONF_TIME_ON, cg.uint32), (CONF_TIME_OFF, cg.uint32)),
)
# delayed_on and delayed_off: the delayed value waits, the other drops the pending output and passes
_PASS_AT_ONCE = "return binary_sensor::cancel_and_pass(filter, x);"
_DELAY_ON = automation.ApplyCall(
"if (x) return binary_sensor::delay_output(filter, {}, x);", _DELAY
)
_DELAY_OFF = automation.ApplyCall(
"if (!x) return binary_sensor::delay_output(filter, {}, x);", _DELAY
)
@register_filter(
@@ -232,26 +238,8 @@ async def delayed_on_off_filter_to_code(config: Any, filter_id: ID) -> MockObj:
)
@register_filter(
"delayed_on", DelayedOnFilter, cv.templatable(cv.positive_time_period_milliseconds)
)
async def delayed_on_filter_to_code(config, filter_id):
var = cg.new_Pvariable(filter_id)
template_ = await cg.templatable(config, [], cg.uint32)
cg.add(var.set_delay(template_))
return var
@register_filter(
"delayed_off",
DelayedOffFilter,
cv.templatable(cv.positive_time_period_milliseconds),
)
async def delayed_off_filter_to_code(config, filter_id):
var = cg.new_Pvariable(filter_id)
template_ = await cg.templatable(config, [], cg.uint32)
cg.add(var.set_delay(template_))
return var
APPLY_FILTER.register("delayed_on", _DELAY_SCHEMA, _DELAY_ON, _PASS_AT_ONCE)
APPLY_FILTER.register("delayed_off", _DELAY_SCHEMA, _DELAY_OFF, _PASS_AT_ONCE)
@register_filter(
+5 -20
View File
@@ -37,32 +37,17 @@ optional<bool> delay_output(Filter *filter, uint32_t delay, bool value) {
return {};
}
optional<bool> cancel_and_pass(Filter *filter, bool value) {
App.scheduler.cancel_timeout(filter);
return value;
}
void TimeoutFilter::input(bool value) {
App.scheduler.set_timeout(this, this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
// Every input re-arms the timeout, including repeats, so this filter must not de-dup
this->output(value);
}
optional<bool> DelayedOnFilter::new_value(bool value) {
if (value) {
App.scheduler.set_timeout(this, this->delay_.value(), [this]() { this->output(true); });
return {};
} else {
App.scheduler.cancel_timeout(this);
return false;
}
}
optional<bool> DelayedOffFilter::new_value(bool value) {
if (!value) {
App.scheduler.set_timeout(this, this->delay_.value(), [this]() { this->output(false); });
return {};
} else {
App.scheduler.cancel_timeout(this);
return true;
}
}
optional<bool> InvertFilter::new_value(bool value) { return !value; }
// AutorepeatFilterBase
+2 -26
View File
@@ -42,32 +42,6 @@ class TimeoutFilter : public Filter {
TemplatableFn<uint32_t> timeout_delay_{};
};
class DelayedOnFilter : public Filter {
public:
// User provided, not "= default": `new(p) DelayedOnFilter()` would zero-fill .bss that is already zero.
DelayedOnFilter() {}
optional<bool> new_value(bool value) override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected:
TemplatableFn<uint32_t> delay_{};
};
class DelayedOffFilter : public Filter {
public:
// User provided, not "= default": `new(p) DelayedOffFilter()` would zero-fill .bss that is already zero.
DelayedOffFilter() {}
optional<bool> new_value(bool value) override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected:
TemplatableFn<uint32_t> delay_{};
};
class InvertFilter : public Filter {
public:
optional<bool> new_value(bool value) override;
@@ -127,6 +101,8 @@ class LambdaFilter : public Filter {
/// Codegen use only: output `value` from `filter` after `delay` ms, replacing its pending output, and return empty
/// to stop the chain for now.
optional<bool> delay_output(Filter *filter, uint32_t delay, bool value);
/// Codegen use only: drop the pending output of `filter` and pass `value` on at once.
optional<bool> cancel_and_pass(Filter *filter, bool value);
/// Codegen use only: the filter's work and constants are one generated function, so the object is only the base.
/// The function gets the filter, so it can schedule output on it.
@@ -7,8 +7,6 @@
#include "esphome/core/automation.h"
#include "esphome/components/network/ip_address.h"
#include <atomic>
#ifdef USE_ETHERNET
#ifdef USE_ESP32
@@ -368,9 +366,6 @@ class EthernetComponent final : public Component {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
StaticVector<EthernetIPStateListener *, ESPHOME_ETHERNET_IP_STATE_LISTENERS> ip_state_listeners_;
#ifdef USE_ESP32
std::atomic<uint8_t> ip_state_pending_{0};
#endif
#endif
#ifdef USE_ETHERNET_CONNECT_TRIGGER
@@ -129,14 +129,6 @@ void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *me
void EthernetComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
// The IP events arrive on the event task; the listeners publish states, so notify them here
if (this->ip_state_pending_.load(std::memory_order_acquire) != 0) {
this->ip_state_pending_.store(0, std::memory_order_relaxed);
this->notify_ip_state_listeners_();
}
#endif
switch (this->state_) {
case EthernetComponentState::STOPPED:
if (this->started_) {
@@ -778,8 +770,7 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
event_name = "ETH connected";
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
global_eth_component->enable_loop_soon_any_context();
global_eth_component->notify_ip_state_listeners_();
#endif
#if USE_NETWORK_IPV6
// Start SLAAC on link-up, not after the DHCPv4 lease. This also restores the
@@ -807,9 +798,6 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
const esp_netif_ip_info_t *ip_info = &event->ip_info;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip));
global_eth_component->got_ipv4_address_ = true;
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
#endif
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
@@ -817,6 +805,9 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
global_eth_component->connected_ = true;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif /* USE_NETWORK_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#if USE_NETWORK_IPV6
@@ -828,9 +819,6 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
// after a link flap fires another event for the same address.
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
global_eth_component->ipv6_count_ = esp_netif_get_all_ip6(global_eth_component->eth_netif_, if_ip6s);
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
#endif
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
@@ -839,6 +827,9 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#endif /* USE_NETWORK_IPV6 */
+2 -6
View File
@@ -5,15 +5,13 @@ from esphome.const import CONF_MODEL
from esphome.types import ConfigType
AUTO_LOAD = ["climate_ir"]
CODEOWNERS = ["@glmnet", "@arpena", "@Rodrigoah"]
CODEOWNERS = ["@glmnet", "@arpena"]
whirlpool_ns = cg.esphome_ns.namespace("whirlpool")
WhirlpoolClimate = whirlpool_ns.class_("WhirlpoolClimate", climate_ir.ClimateIR)
Model = whirlpool_ns.enum("Model")
MODELS = Model.options(
"DG11J1-3A", "DG11J1-91", "HISENSE_ARG", "HITACHI_HCRA31NEWH", prefix="MODEL_"
)
MODELS = Model.options("DG11J1-3A", "DG11J1-91", "HISENSE_ARG", prefix="MODEL_")
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(WhirlpoolClimate).extend(
{
@@ -28,5 +26,3 @@ async def to_code(config: ConfigType) -> None:
# cv.enum keeps the YAML key as a str subclass; the MockObj value cannot be compared
if config[CONF_MODEL] == "HISENSE_ARG":
cg.add_define("USE_WHIRLPOOL_HISENSE_ARG")
elif config[CONF_MODEL] == "HITACHI_HCRA31NEWH":
cg.add_define("USE_WHIRLPOOL_HITACHI_HCRA31NEWH")
+15 -61
View File
@@ -2,8 +2,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome::whirlpool {
ESPHOME_LOG_TAG(TAG, "whirlpool.climate");
@@ -46,17 +44,6 @@ static constexpr uint8_t HISENSE_ARG_POWER_TOGGLE = 0x01; // byte 15
static constexpr uint8_t HISENSE_ARG_SETTINGS_ONLY = 0x02; // byte 15
static constexpr uint8_t HISENSE_ARG_FIXED_TEMP_STEP = 7; // auto and fan-only send 23 C
// Hitachi HCRA31NEWH model: power is a state in every frame
static constexpr uint8_t HITACHI_BYTE10 = 0x20; // byte 10
static constexpr uint8_t HITACHI_COOL = 0x02; // byte 15
static constexpr uint8_t HITACHI_OFF = 0x01; // byte 15
static constexpr uint8_t HITACHI_BYTE18_ON = 0x38; // byte 18
static constexpr uint8_t HITACHI_BYTE18_OFF = 0x28; // byte 18
static constexpr uint8_t HITACHI_BYTE19 = 0x08; // byte 19
// Header, 21 bytes of 8 bits, two dividers and the footer mark
static constexpr uint32_t WHIRLPOOL_FRAME_TIMINGS = 2 + WHIRLPOOL_STATE_LENGTH * 16 + 2 * 2 + 1;
WhirlpoolClimate::WhirlpoolClimate()
: climate_ir::ClimateIR(
WHIRLPOOL_DG11J1_3A_TEMP_MIN, WHIRLPOOL_DG11J1_3A_TEMP_MAX, 1.0f, true, true,
@@ -68,19 +55,19 @@ void WhirlpoolClimate::transmit_state() {
this->last_transmit_time_ = millis(); // setting the time of the last transmission.
uint8_t remote_state[WHIRLPOOL_STATE_LENGTH];
memset(remote_state, 0, sizeof(remote_state));
put_be16(remote_state, this->remote_id_());
remote_state[0] = 0x83;
remote_state[1] = 0x06;
remote_state[6] = 0x80;
remote_state[18] = 0x08;
remote_state[18] = 0x08; // the same on every supported model
const bool powered_on = this->mode != climate::CLIMATE_MODE_OFF;
// Hitachi sends power as a state instead of a toggle
const bool power_toggle = !this->is_hitachi_hcra31newh_() && powered_on != this->powered_on_assumed;
auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
const bool power_toggle = powered_on != this->powered_on_assumed;
if (power_toggle) {
// Set power toggle command
remote_state[2] = 4;
remote_state[15] = 1;
this->powered_on_assumed = powered_on;
}
this->powered_on_assumed = powered_on;
switch (this->mode) {
case climate::CLIMATE_MODE_HEAT_COOL:
// set fan auto
@@ -106,10 +93,6 @@ void WhirlpoolClimate::transmit_state() {
remote_state[15] = 6;
break;
case climate::CLIMATE_MODE_OFF:
// These remotes keep the cool code in the power-off frame
if (this->is_hisense_arg_() || this->is_hitachi_hcra31newh_())
remote_state[3] = WHIRLPOOL_COOL;
break;
default:
break;
}
@@ -119,7 +102,6 @@ void WhirlpoolClimate::transmit_state() {
// Temperature
auto temp = (uint8_t) roundf(clamp(this->target_temperature, this->temperature_min_(), this->temperature_max_()));
const auto temp_step = (uint8_t) (temp - this->temperature_min_());
this->target_temperature = temp; // publish what the unit was sent
if (hisense_arg && this->fixed_temp_mode_()) {
remote_state[3] |= HISENSE_ARG_FIXED_TEMP_STEP << 4;
} else {
@@ -149,6 +131,7 @@ void WhirlpoolClimate::transmit_state() {
remote_state[2] |= WHIRLPOOL_SWING_MASK;
remote_state[8] = WHIRLPOOL_SWING_BYTE8;
}
this->encode_hisense_arg_(remote_state, power_toggle, temp_step);
} else if (this->send_swing_cmd_) {
if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL || this->swing_mode == climate::CLIMATE_SWING_OFF) {
remote_state[2] |= WHIRLPOOL_SWING_MASK;
@@ -156,11 +139,6 @@ void WhirlpoolClimate::transmit_state() {
}
}
if (hisense_arg)
this->encode_hisense_arg_(remote_state, power_toggle, temp_step);
if (this->is_hitachi_hcra31newh_())
this->encode_hitachi_hcra31newh_(remote_state);
// Checksum
remote_state[13] = xor_bytes(&remote_state[2], 11, remote_state[13]);
remote_state[20] = xor_bytes(&remote_state[14], 6, remote_state[20]);
@@ -170,7 +148,6 @@ void WhirlpoolClimate::transmit_state() {
// Send code
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
data->reserve(WHIRLPOOL_FRAME_TIMINGS);
data->set_carrier_frequency(38000);
@@ -203,6 +180,10 @@ void WhirlpoolClimate::encode_hisense_arg_(uint8_t *remote_state, bool power_tog
remote_state[15] = power_toggle ? HISENSE_ARG_POWER_TOGGLE : HISENSE_ARG_SETTINGS_ONLY;
switch (this->mode) {
case climate::CLIMATE_MODE_OFF:
// The remote keeps the cool code in the power-off frame
remote_state[3] = (remote_state[3] & 0xF0) | WHIRLPOOL_COOL;
break;
case climate::CLIMATE_MODE_DRY:
// The unit runs these fan speeds itself; the user's choice is kept for the next mode change
remote_state[2] &= ~WHIRLPOOL_FAN_MASK;
@@ -231,27 +212,6 @@ void WhirlpoolClimate::encode_hisense_arg_(uint8_t *remote_state, bool power_tog
#endif
}
void WhirlpoolClimate::encode_hitachi_hcra31newh_(uint8_t *remote_state) const {
#ifdef USE_WHIRLPOOL_HITACHI_HCRA31NEWH
remote_state[10] = HITACHI_BYTE10;
remote_state[18] = HITACHI_BYTE18_ON;
remote_state[19] = HITACHI_BYTE19;
// The unit takes the whole state from every frame: only the off frame carries the power bit
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
remote_state[15] = HITACHI_COOL;
break;
case climate::CLIMATE_MODE_OFF:
remote_state[2] |= WHIRLPOOL_POWER;
remote_state[15] = HITACHI_OFF;
remote_state[18] = HITACHI_BYTE18_OFF;
break;
default:
break;
}
#endif
}
bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
// Check if the esp isn't currently transmitting.
if (millis() - this->last_transmit_time_ < 500) {
@@ -274,8 +234,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_GAP))
return false;
}
// The Hitachi remote always sends all 21 bytes; byte 18 holds its power state
if (i == WHIRLPOOL_SECOND_DIVIDER && !data.is_valid() && !this->is_hitachi_hcra31newh_()) {
if (i == WHIRLPOOL_SECOND_DIVIDER && !data.is_valid()) {
// Remote control only sent 14 bytes, nothing more to read, not even the footer
ESP_LOGV(TAG, "Remote control only sent %d bytes", i);
skip_footer = true;
@@ -302,18 +261,13 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
ESP_LOGV_HEX(TAG, remote_state, WHIRLPOOL_STATE_LENGTH, WHIRLPOOL_STATE_LENGTH, "Received: %s");
// verify header remote code
if (encode_uint16(remote_state[0], remote_state[1]) != this->remote_id_())
if (remote_state[0] != 0x83 || remote_state[1] != 0x06)
return false;
// powr on/off button
ESP_LOGV(TAG, "Power: %02X", (remote_state[2] & WHIRLPOOL_POWER));
if (this->is_hitachi_hcra31newh_()) {
// Power is a state: byte 18 says whether the unit is off
this->powered_on_assumed = remote_state[18] != HITACHI_BYTE18_OFF;
if (!this->powered_on_assumed)
this->mode = climate::CLIMATE_MODE_OFF;
} else if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) {
if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) {
auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
if (powered_on) {
@@ -355,7 +309,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
int temp = remote_state[3] & 0xF0;
ESP_LOGVV(TAG, "Temperature Raw: %02X", temp);
temp = (uint8_t) temp >> 4;
temp = std::min(temp + static_cast<int>(this->temperature_min_()), static_cast<int>(this->temperature_max_()));
temp += static_cast<int>(this->temperature_min_());
ESP_LOGVV(TAG, "Temperature Climate: %u", temp);
this->target_temperature = temp;
}
+4 -19
View File
@@ -6,10 +6,9 @@ namespace esphome::whirlpool {
/// Simple enum to represent models.
enum Model {
MODEL_DG11J1_3A = 0, /// Temperature range is from 18 to 32
MODEL_DG11J1_91 = 1, /// Temperature range is from 16 to 30
MODEL_HISENSE_ARG = 2, /// Hisense/BGH (Argentina), 16 to 30
MODEL_HITACHI_HCRA31NEWH = 3, /// Hitachi with the HCRA31NEWH remote, 16 to 30
MODEL_DG11J1_3A = 0, /// Temperature range is from 18 to 32
MODEL_DG11J1_91 = 1, /// Temperature range is from 16 to 30
MODEL_HISENSE_ARG = 2, /// Hisense/BGH (Argentina), 16 to 30
};
// Temperature
@@ -30,9 +29,7 @@ class WhirlpoolClimate final : public climate_ir::ClimateIR {
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override {
// The toggle remotes flip swing on every press, so only send one when the mode really changes
auto swing = call.get_swing_mode();
this->send_swing_cmd_ = swing.has_value() && *swing != this->swing_mode;
send_swing_cmd_ = call.get_swing_mode().has_value();
climate_ir::ClimateIR::control(call);
}
@@ -58,22 +55,10 @@ class WhirlpoolClimate final : public climate_ir::ClimateIR {
return false;
#endif
}
/// False when the model is compiled out, so the compiler drops the Hitachi branches.
bool is_hitachi_hcra31newh_() const {
#ifdef USE_WHIRLPOOL_HITACHI_HCRA31NEWH
return this->model_ == MODEL_HITACHI_HCRA31NEWH;
#else
return false;
#endif
}
/// The remote ID sent in bytes 0 and 1.
uint16_t remote_id_() const { return this->is_hitachi_hcra31newh_() ? 0xA959 : 0x8306; }
/// Modes in which the Hisense ARG model sends a fixed temperature.
bool fixed_temp_mode_() const;
/// Add the Hisense ARG bytes on top of the common whirlpool frame.
void encode_hisense_arg_(uint8_t *remote_state, bool power_toggle, uint8_t temp_step) const;
/// Add the Hitachi HCRA31NEWH bytes on top of the common whirlpool frame.
void encode_hitachi_hcra31newh_(uint8_t *remote_state) const;
/// Set the time of the last transmission.
uint32_t last_transmit_time_{};
+1 -1
View File
@@ -563,7 +563,7 @@ class WiFiComponent final : public Component {
// (In most use cases you won't need these)
/// Setup WiFi interface.
void setup() override;
#if defined(USE_ESP32) || defined(USE_LN882X) || defined(USE_BK72XX)
#if defined(USE_LN882X) || defined(USE_BK72XX)
void on_powerdown() override;
#endif
void start();
@@ -1635,21 +1635,6 @@ bool WiFiComponent::wifi_dpp_start_listen_() {
void WiFiComponent::wifi_dpp_stop_listen_() { esp_supp_dpp_stop_listen(); }
#endif
void WiFiComponent::on_powerdown() {
// Leave the AP before esp_restart() or deep sleep stops Wi-Fi. esp_wifi_stop() while associated
// makes the ESP32 retransmit its Disassociation up to the retry limit although the AP ACKs it
// (32 copies with protected management frames), which some APs handle badly.
// Upstream bug: https://github.com/espressif/esp-idf/issues/19443 - remove this once it is fixed
// in the ESP-IDF versions we support.
// Ask the driver: s_sta_connected lags until the main loop has handled the connect event.
wifi_ap_record_t ap_info;
if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK)
return;
ESP_LOGD(TAG, "Disconnecting before powerdown");
esp_wifi_disconnect();
delay(100); // NOLINT: once per reboot or deep sleep, lets the disassociation go out
}
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
bssid_t WiFiComponent::wifi_bssid() {
-2
View File
@@ -294,8 +294,6 @@ void Application::reboot() {
for (auto &component : std::ranges::reverse_view(this->components_)) {
component->on_shutdown();
}
// No teardown on a forced reboot, but still power hardware down (Wi-Fi leaves the AP)
this->run_powerdown_hooks();
arch_restart();
}
void Application::safe_reboot() {
+2 -4
View File
@@ -185,12 +185,10 @@ class Component {
*/
virtual bool teardown() { return true; }
/** Called last before a reboot or deep sleep to power down hardware.
/** Called after teardown is complete to power down hardware.
*
* On a safe shutdown this is called after all components have finished their teardown process,
* This is called after all components have finished their teardown process,
* making it safe to power down hardware like ethernet PHY.
* On a forced reboot (Application::reboot()) it is called right after on_shutdown() with no
* teardown, so implementations must not assume teardown() has run.
*/
virtual void on_powerdown() {}
-1
View File
@@ -387,7 +387,6 @@
#define USE_WATER_HEATER
#define USE_WATER_HEATER_VISUAL_OVERRIDES
#define USE_WHIRLPOOL_HISENSE_ARG
#define USE_WHIRLPOOL_HITACHI_HCRA31NEWH
#define USE_ZWAVE_PROXY
// Feature flags which do not work for zephyr
@@ -85,3 +85,19 @@ def test_delayed_on_off_shorthand(generate_main) -> None:
assert re.search(
rf"static uint32_t {fn}\(\) {{\n(.*\n)? static uint32_t n = 0;", main_cpp
)
def test_delayed_on_and_off(generate_main) -> None:
"""Only the delayed value waits; the other drops the pending output and passes."""
main_cpp = generate_main(
"tests/component_tests/binary_sensor/test_delayed_on_off.yaml"
)
assert (
" if (x) return binary_sensor::delay_output(filter, 20, x);\n"
" return binary_sensor::cancel_and_pass(filter, x);\n"
) in main_cpp
assert (
" if (!x) return binary_sensor::delay_output(filter, 30, x);\n"
" return binary_sensor::cancel_and_pass(filter, x);\n"
) in main_cpp
@@ -18,3 +18,9 @@ binary_sensor:
- delayed_on_off: !lambda |-
static uint32_t n = 0;
return ++n * 100;
- platform: template
id: delayed_on_constant
lambda: return true;
filters:
- delayed_on: 20ms
- delayed_off: 30ms
@@ -4,6 +4,8 @@
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/binary_sensor/filter.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
namespace esphome::binary_sensor::testing {
@@ -50,4 +52,47 @@ TEST(BinarySensorApplyFilter, PassesItselfToTheFunction) {
EXPECT_FALSE(sensor.state);
}
static void run_scheduler_after(uint32_t ms) {
delay(ms);
App.scheduler.call(millis());
}
// Mirrors the code generated for `delayed_on: 10ms`
static optional<bool> delayed_on_10ms(Filter *filter, bool x) {
return x ? delay_output(filter, 10, true) : cancel_and_pass(filter, false);
}
TEST(BinarySensorDelayOutput, UndelayedEdgeCancelsAndPassesAtOnce) {
BinarySensor sensor;
ApplyFilter<delayed_on_10ms> delayed_on;
sensor.add_filter(&delayed_on);
sensor.publish_state(true);
EXPECT_FALSE(sensor.has_state());
sensor.publish_state(false);
ASSERT_TRUE(sensor.has_state());
EXPECT_FALSE(sensor.state);
run_scheduler_after(30);
EXPECT_FALSE(sensor.state);
}
// Mirrors the code generated for `delayed_on_off: 0ms`
static optional<bool> delayed_on_off_0ms(Filter *filter, bool x) { return delay_output(filter, 0, x); }
// A 0 ms delay still goes through the scheduler, so edges within one pass coalesce
TEST(BinarySensorDelayOutput, ZeroDelayWaitsForTheSchedulerAndCoalesces) {
BinarySensor sensor;
ApplyFilter<delayed_on_off_0ms> delayed_on_off;
sensor.add_filter(&delayed_on_off);
int changes = 0;
sensor.add_on_state_callback([&changes](bool) { changes++; });
sensor.publish_state(true);
sensor.publish_state(false);
sensor.publish_state(true);
EXPECT_FALSE(sensor.has_state());
run_scheduler_after(1);
ASSERT_TRUE(sensor.has_state());
EXPECT_TRUE(sensor.state);
EXPECT_EQ(changes, 1);
}
} // namespace esphome::binary_sensor::testing
@@ -32,17 +32,6 @@ TEST(BinarySensorInvalidate, RepeatedValuePassesTheFiltersAfterInvalidate) {
EXPECT_FALSE(sensor.state);
}
TEST(BinarySensorInvalidate, PendingDelayedOutputIsDropped) {
BinarySensor sensor;
DelayedOnFilter delayed_on;
delayed_on.set_delay([]() -> uint32_t { return 10; });
sensor.add_filter(&delayed_on);
sensor.publish_state(true);
sensor.invalidate_state();
run_scheduler_after(30);
EXPECT_FALSE(sensor.has_state());
}
// Mirrors the code generated for `delayed_on_off: 10ms`
static optional<bool> delayed_on_off_10ms(Filter *filter, bool x) { return delay_output(filter, 10, x); }
@@ -2,7 +2,3 @@ packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
# IPv6 with an IP sensor: compiles the IPv6 event handler's listener notification
network:
enable_ipv6: true
+1 -2
View File
@@ -10,8 +10,7 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.auto_load = ["climate_ir", "climate"]
async def to_code_testing(config: ConfigType) -> None:
# Compiles in the Hisense ARG and Hitachi models, which climate.py adds only when configured
# Compiles in the Hisense ARG model, which climate.py adds only when it is configured
cg.add_define("USE_WHIRLPOOL_HISENSE_ARG")
cg.add_define("USE_WHIRLPOOL_HITACHI_HCRA31NEWH")
manifest.to_code = to_code_testing
-5
View File
@@ -8,8 +8,3 @@ climate:
model: HISENSE_ARG
transmitter_id: xmitr
receiver_id: rcvr
- platform: whirlpool
name: Whirlpool Hitachi HCRA31NEWH Climate
model: HITACHI_HCRA31NEWH
transmitter_id: xmitr
receiver_id: rcvr
@@ -252,9 +252,6 @@ TEST(WhirlpoolHisenseArg, SwingIsSentOnEveryTransmit) {
TEST(WhirlpoolDG11J13A, SwingIsSentOnlyWhenItChanges) {
Fixture f(MODEL_DG11J1_3A);
f.command(climate::CLIMATE_MODE_COOL, 24, climate::CLIMATE_FAN_LOW, climate::CLIMATE_SWING_OFF);
EXPECT_EQ(f.last_frame()[2] & 0x80, 0) << "swing is already off";
f.command(climate::CLIMATE_MODE_COOL, 24, climate::CLIMATE_FAN_LOW, climate::CLIMATE_SWING_VERTICAL);
Frame frame = f.last_frame();
EXPECT_EQ(frame[2] & 0x80, 0x80);
@@ -264,9 +261,6 @@ TEST(WhirlpoolDG11J13A, SwingIsSentOnlyWhenItChanges) {
frame = f.last_frame();
EXPECT_EQ(frame[2] & 0x80, 0);
EXPECT_EQ(frame[8], 0x00);
f.command(climate::CLIMATE_MODE_COOL, 25, climate::CLIMATE_FAN_LOW, climate::CLIMATE_SWING_VERTICAL);
EXPECT_EQ(f.last_frame()[2] & 0x80, 0) << "swing is already vertical";
}
// Cool 24 C (step 8), fan low, powered on assumed
@@ -391,90 +385,4 @@ TEST(WhirlpoolHisenseArg, EveryTransmitHasValidChecksums) {
}
}
// Cool 24 C (step 8), fan low, unit on
static Frame hitachi_cool_frame() {
return with_checksums({0xA9, 0x59, 0x03, 0x82, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x20,
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x38, 0x08, 0x00});
}
// The off frame: power bit, cool code, byte 15 0x01 and byte 18 0x28
static Frame hitachi_off_frame() {
return with_checksums({0xA9, 0x59, 0x07, 0x82, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x20,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x28, 0x08, 0x00});
}
TEST(WhirlpoolHitachiHCRA31NEWH, CoolAndOffMatchKnownFrames) {
Fixture f(MODEL_HITACHI_HCRA31NEWH);
f.command(climate::CLIMATE_MODE_COOL, 24, climate::CLIMATE_FAN_LOW);
// Turning on sends the full state with no power bit
expect_frame(f.last_frame(), hitachi_cool_frame());
f.command(climate::CLIMATE_MODE_OFF, 24, climate::CLIMATE_FAN_LOW);
expect_frame(f.last_frame(), hitachi_off_frame());
// A second off command still sends the power bit
f.command(climate::CLIMATE_MODE_OFF, 24, climate::CLIMATE_FAN_LOW);
EXPECT_EQ(f.last_frame()[2] & 0x04, 0x04);
f.command(climate::CLIMATE_MODE_HEAT, 24, climate::CLIMATE_FAN_LOW);
const Frame frame = f.last_frame();
EXPECT_EQ(frame[2] & 0x04, 0);
EXPECT_EQ(frame[3], 0x80);
EXPECT_EQ(frame[15], 0x06);
EXPECT_EQ(frame[18], 0x38);
}
TEST(WhirlpoolHitachiHCRA31NEWH, ReceivePowerIsAState) {
Fixture f(MODEL_HITACHI_HCRA31NEWH);
ASSERT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
// An on frame restores the mode even though it has no power bit
EXPECT_TRUE(receive(f.climate, hitachi_cool_frame()));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_COOL);
EXPECT_FLOAT_EQ(f.climate.target_temperature, 24);
EXPECT_EQ(f.climate.fan_mode, climate::CLIMATE_FAN_LOW);
// The same frame again keeps it on
EXPECT_TRUE(receive(f.climate, hitachi_cool_frame()));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_COOL);
EXPECT_TRUE(receive(f.climate, hitachi_off_frame()));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
// A repeated off frame does not toggle
EXPECT_TRUE(receive(f.climate, hitachi_off_frame()));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
}
TEST(WhirlpoolHitachiHCRA31NEWH, ReceiveChecksRemoteId) {
Fixture hitachi(MODEL_HITACHI_HCRA31NEWH);
EXPECT_FALSE(receive(hitachi.climate, hisense_cool_frame()));
Fixture whirlpool(MODEL_DG11J1_91);
EXPECT_FALSE(receive(whirlpool.climate, hitachi_cool_frame()));
}
TEST(WhirlpoolHitachiHCRA31NEWH, ReceivedTemperatureIsClamped) {
Fixture f(MODEL_HITACHI_HCRA31NEWH);
Frame frame = hitachi_cool_frame();
frame[3] = 0xF2; // step 15 would be 31 C
EXPECT_TRUE(receive(f.climate, with_checksums(frame)));
EXPECT_FLOAT_EQ(f.climate.target_temperature, 30);
}
TEST(WhirlpoolDG11J13A, TargetIsClampedAndRoundedToWhatWasSent) {
Fixture f(MODEL_DG11J1_3A);
f.command(climate::CLIMATE_MODE_COOL, 35, climate::CLIMATE_FAN_LOW);
EXPECT_FLOAT_EQ(f.climate.target_temperature, 32);
f.command(climate::CLIMATE_MODE_COOL, 24.4f, climate::CLIMATE_FAN_LOW);
EXPECT_FLOAT_EQ(f.climate.target_temperature, 24);
}
TEST(WhirlpoolHitachiHCRA31NEWH, ReceiveRejectsShortFrame) {
Fixture f(MODEL_HITACHI_HCRA31NEWH);
remote_base::RawTimings raw = waveform(hitachi_cool_frame());
// Cut after the second divider: 14 bytes, no footer
raw.resize(2 + 14 * 16 + 2 * 2);
remote_base::RemoteReceiveData data(raw, 25, remote_base::TOLERANCE_MODE_PERCENTAGE);
EXPECT_FALSE(static_cast<remote_base::RemoteReceiverListener &>(f.climate).on_receive(data));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
}
} // namespace esphome::whirlpool::testing
@@ -1,23 +0,0 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/tests"]
MULTI_CONF = True
reboot_hook_recorder_ns = cg.esphome_ns.namespace("reboot_hook_recorder")
RebootHookRecorder = reboot_hook_recorder_ns.class_("RebootHookRecorder", cg.Component)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(RebootHookRecorder),
cv.Required(CONF_NAME): cv.string,
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_name(config[CONF_NAME]))
@@ -1,10 +0,0 @@
#include "reboot_hook_recorder.h"
#include "esphome/core/log.h"
namespace esphome::reboot_hook_recorder {
ESPHOME_LOG_TAG(TAG, "reboot_hook_recorder");
void RebootHookRecorder::record_(const char *hook) { ESP_LOGI(TAG, "HOOK %s %s", this->name_, hook); }
} // namespace esphome::reboot_hook_recorder
@@ -1,33 +0,0 @@
#pragma once
#include "esphome/core/component.h"
namespace esphome::reboot_hook_recorder {
/// Logs every shutdown lifecycle hook it receives, so a test can check their order.
class RebootHookRecorder : public Component {
public:
void set_name(const char *name) { this->name_ = name; }
void on_safe_shutdown() override { this->record_("on_safe_shutdown"); }
void on_shutdown() override { this->record_("on_shutdown"); }
// Needs a second call to finish, like a component waiting for a connection to close
bool teardown() override {
if (++this->teardown_calls_ < 2) {
this->record_("teardown_pending");
return false;
}
if (this->teardown_calls_ == 2)
this->record_("teardown_done");
return true;
}
void on_powerdown() override { this->record_("on_powerdown"); }
protected:
void record_(const char *hook);
const char *name_{""};
uint8_t teardown_calls_{0};
};
} // namespace esphome::reboot_hook_recorder
@@ -1,19 +0,0 @@
esphome:
name: reboot-hooks-forced
host:
logger:
level: DEBUG
api:
# Reboot once the test has connected, so that it sees every hook
on_client_connected:
- delay: 100ms
- lambda: App.reboot();
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
reboot_hook_recorder:
- name: first
- name: second
@@ -1,19 +0,0 @@
esphome:
name: reboot-hooks-safe
host:
logger:
level: DEBUG
api:
# Reboot once the test has connected, so that it sees every hook
on_client_connected:
- delay: 100ms
- lambda: App.safe_reboot();
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
reboot_hook_recorder:
- name: first
- name: second
@@ -1,80 +0,0 @@
"""Test the order of the shutdown lifecycle hooks on a forced and on a safe reboot."""
import re
import pytest
from .log_utils import LineWaiter
from .types import APIClientConnectedFactory, RunCompiledFunction
from .wait_utils import wait_or_fail
HOOK_PATTERN = re.compile(r"HOOK (\w+) (\w+)")
# Hooks run in reverse component order, so "first" gets each hook last.
LAST_HOOK = "HOOK first on_powerdown"
async def _record_reboot_hooks(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> list[tuple[str, str]]:
"""Connect, which makes the device reboot, and return its (component, hook) calls in order."""
waiter = LineWaiter()
hooks: list[tuple[str, str]] = []
def on_line(line: str) -> None:
if match := HOOK_PATTERN.search(line):
hooks.append((match[1], match[2]))
waiter.callback(line)
async with (
run_compiled(yaml_config, line_callback=on_line),
api_client_connected(),
):
await wait_or_fail(
waiter.wait_for(LAST_HOOK),
5.0,
"Device did not run the powerdown hooks",
hooks=hooks,
)
return hooks
@pytest.mark.asyncio
async def test_reboot_hook_order_forced(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""A forced reboot runs on_shutdown then on_powerdown, with no safe shutdown or teardown."""
hooks = await _record_reboot_hooks(yaml_config, run_compiled, api_client_connected)
assert hooks == [
("second", "on_shutdown"),
("first", "on_shutdown"),
("second", "on_powerdown"),
("first", "on_powerdown"),
]
@pytest.mark.asyncio
async def test_reboot_hook_order_safe(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""A safe reboot powers down only after every component has finished its teardown."""
hooks = await _record_reboot_hooks(yaml_config, run_compiled, api_client_connected)
assert hooks == [
("second", "on_safe_shutdown"),
("first", "on_safe_shutdown"),
("second", "on_shutdown"),
("first", "on_shutdown"),
("second", "teardown_pending"),
("first", "teardown_pending"),
("second", "teardown_done"),
("first", "teardown_done"),
("second", "on_powerdown"),
("first", "on_powerdown"),
]