[counter] Add persistent counter sensor (#19746)

This commit is contained in:
Clyde Stubbs
2026-09-27 18:25:54 +10:00
committed by GitHub
parent c26f2d28e6
commit b8af6cb005
14 changed files with 485 additions and 0 deletions
@@ -0,0 +1,17 @@
esphome:
name: test
on_boot:
then:
- counter.set_value: 5
- counter.set_value:
value: 7
- counter.increment:
value: -3
esp32:
board: esp32dev
sensor:
- platform: counter
id: only_counter
name: Only Counter
@@ -0,0 +1,42 @@
esphome:
name: test
on_boot:
then:
- counter.set_value:
id: counter_a
value: 100
- counter.set_value:
id: counter_b
value: -5000000000
- counter.increment: counter_a
- counter.increment:
id: counter_a
value: -7
- counter.increment:
id: counter_b
value: !lambda "return 3;"
esp32:
board: esp32dev
sensor:
- platform: template
id: source_sensor
lambda: return 1.0;
- platform: counter
id: counter_a
name: Counter A
sensor: source_sensor
- platform: counter
id: counter_b
name: Counter B
restore: false
initial_value: -5000000000
- platform: counter
id: counter_c
name: Counter C
binary_sensor: source_binary_sensor
binary_sensor:
- platform: template
id: source_binary_sensor
@@ -0,0 +1,99 @@
"""Tests for the counter sensor."""
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome import config_validation as cv
from esphome.components.counter.sensor import CONFIG_SCHEMA, COUNTER_VALUE
INT64_MAX = 2**63 - 1
def test_counter_constructor_arguments(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Restore (on by default) and initial value (zero by default) are constructor arguments."""
main_cpp = generate_main(component_config_path("counter_test.yaml"))
assert "new(counter_a) counter::CounterSensor(true, 0);" in main_cpp
assert "new(counter_b) counter::CounterSensor(false, -5000000000LL);" in main_cpp
def test_counter_sensor_option_registers_source(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Only the counter with a sensor option counts that sensor's updates."""
main_cpp = generate_main(component_config_path("counter_test.yaml"))
assert "counter_a->count_updates_from(source_sensor);" in main_cpp
assert main_cpp.count("count_updates_from") == 1
def test_counter_binary_sensor_option_registers_source(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Only the counter with a binary_sensor option counts that sensor's changes to true."""
main_cpp = generate_main(component_config_path("counter_test.yaml"))
assert "counter_c->count_true_from(source_binary_sensor);" in main_cpp
assert main_cpp.count("count_true_from") == 1
def test_counter_actions(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Values reach the counter as 64-bit integers; increment defaults to one."""
main_cpp = generate_main(component_config_path("counter_test.yaml"))
assert "::counter_a->set_value(100);" in main_cpp
assert "::counter_b->set_value(-5000000000LL);" in main_cpp
assert "::counter_a->increment(1);" in main_cpp
assert "::counter_a->increment(-7);" in main_cpp
assert "::counter_b->increment(static_cast<int64_t>(3));" in main_cpp
def test_counter_actions_without_id_use_only_counter(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""With one counter configured, the id may be left out of an action."""
main_cpp = generate_main(component_config_path("counter_single.yaml"))
assert "::only_counter->set_value(5);" in main_cpp
assert "::only_counter->set_value(7);" in main_cpp
assert "::only_counter->increment(-3);" in main_cpp
@pytest.mark.parametrize("value", [INT64_MAX + 1, -INT64_MAX - 1, 1.5])
def test_counter_initial_value_must_be_int64(value: float) -> None:
with pytest.raises(cv.Invalid):
CONFIG_SCHEMA({"id": "c1", "name": "C1", "initial_value": value})
def test_counter_cannot_count_itself() -> None:
"""A counter watching its own updates would recurse forever."""
with pytest.raises(cv.Invalid, match="cannot count its own updates"):
CONFIG_SCHEMA({"id": "c1", "name": "C1", "sensor": "c1"})
def test_counter_accepts_other_source() -> None:
config = CONFIG_SCHEMA({"id": "c1", "name": "C1", "sensor": "other"})
assert config["sensor"].id == "other"
@pytest.mark.parametrize("value", [0, 1, -1, INT64_MAX, -INT64_MAX])
def test_counter_value_accepts_int64_range(value: int) -> None:
assert COUNTER_VALUE(value) == value
@pytest.mark.parametrize("value", [INT64_MAX + 1, -INT64_MAX - 1, 1.5])
def test_counter_value_rejects_out_of_range(value: float) -> None:
with pytest.raises(cv.Invalid):
COUNTER_VALUE(value)