mirror of
https://github.com/esphome/esphome.git
synced 2026-09-28 23:40:25 +00:00
[counter] Add persistent counter sensor (#19746)
This commit is contained in:
@@ -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)
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/counter/counter_sensor.h"
|
||||
|
||||
namespace esphome::counter::testing {
|
||||
|
||||
constexpr int64_t INT64_MAX_VALUE = std::numeric_limits<int64_t>::max();
|
||||
constexpr int64_t INT64_MIN_VALUE = std::numeric_limits<int64_t>::min();
|
||||
|
||||
/// Stands in for a binary sensor: reports each state to its callbacks.
|
||||
struct FakeBinarySource {
|
||||
template<typename F> void add_on_state_callback(F &&callback) { this->callbacks_.add(std::forward<F>(callback)); }
|
||||
void publish(bool state) { this->callbacks_.call(state); }
|
||||
CallbackManager<void(bool)> callbacks_;
|
||||
};
|
||||
|
||||
// Restore is off so no preference storage is needed.
|
||||
class CounterTest : public ::testing::Test {
|
||||
protected:
|
||||
CounterSensor counter_{false, 0};
|
||||
};
|
||||
|
||||
} // namespace esphome::counter::testing
|
||||
@@ -0,0 +1,37 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- counter.set_value:
|
||||
id: counter_sensor
|
||||
value: 100
|
||||
- counter.increment: counter_sensor
|
||||
- counter.increment:
|
||||
id: counter_sensor
|
||||
value: -5
|
||||
- counter.increment:
|
||||
id: counter_sensor
|
||||
value: !lambda "return 5000000000LL;"
|
||||
|
||||
sensor:
|
||||
- platform: counter
|
||||
id: counter_sensor
|
||||
name: Counter
|
||||
- platform: counter
|
||||
id: counter_no_restore
|
||||
name: Counter No Restore
|
||||
restore: false
|
||||
initial_value: 10
|
||||
- platform: template
|
||||
id: source_sensor
|
||||
lambda: return 1.0;
|
||||
update_interval: 1s
|
||||
- platform: counter
|
||||
name: Counter Of Sensor
|
||||
sensor: source_sensor
|
||||
- platform: counter
|
||||
name: Counter Of Binary Sensor
|
||||
binary_sensor: source_binary_sensor
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: source_binary_sensor
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::counter::testing {
|
||||
|
||||
TEST_F(CounterTest, StartsAtZero) {
|
||||
this->counter_.setup();
|
||||
EXPECT_EQ(this->counter_.state, 0.0f);
|
||||
}
|
||||
|
||||
TEST(CounterInitialValue, PublishedAtSetupWhenNotRestoring) {
|
||||
CounterSensor counter(false, -12);
|
||||
counter.setup();
|
||||
EXPECT_EQ(counter.state, -12.0f);
|
||||
counter.increment();
|
||||
EXPECT_EQ(counter.state, -11.0f);
|
||||
}
|
||||
|
||||
TEST_F(CounterTest, SetValuePublishesState) {
|
||||
this->counter_.set_value(42);
|
||||
EXPECT_EQ(this->counter_.state, 42.0f);
|
||||
}
|
||||
|
||||
TEST_F(CounterTest, IncrementDefaultsToOne) {
|
||||
this->counter_.increment();
|
||||
this->counter_.increment();
|
||||
EXPECT_EQ(this->counter_.state, 2.0f);
|
||||
}
|
||||
|
||||
TEST_F(CounterTest, IncrementAcceptsNegativeAmounts) {
|
||||
this->counter_.set_value(10);
|
||||
this->counter_.increment(-25);
|
||||
EXPECT_EQ(this->counter_.state, -15.0f);
|
||||
}
|
||||
|
||||
TEST_F(CounterTest, ValueBeyondInt32) {
|
||||
this->counter_.set_value(5000000000LL);
|
||||
this->counter_.increment(5000000000LL);
|
||||
EXPECT_EQ(this->counter_.state, 1.0e10f);
|
||||
}
|
||||
|
||||
TEST_F(CounterTest, IncrementWrapsAtInt64Limits) {
|
||||
this->counter_.set_value(INT64_MAX_VALUE);
|
||||
this->counter_.increment(1);
|
||||
EXPECT_EQ(this->counter_.state, static_cast<float>(INT64_MIN_VALUE));
|
||||
|
||||
this->counter_.set_value(INT64_MIN_VALUE);
|
||||
this->counter_.increment(-1);
|
||||
EXPECT_EQ(this->counter_.state, static_cast<float>(INT64_MAX_VALUE));
|
||||
}
|
||||
|
||||
TEST_F(CounterTest, CountsEachPublishFromSource) {
|
||||
sensor::Sensor source;
|
||||
this->counter_.count_updates_from(&source);
|
||||
|
||||
// The counted value is unrelated to what the source publishes.
|
||||
source.publish_state(10.0f);
|
||||
source.publish_state(10.0f);
|
||||
source.publish_state(-3.5f);
|
||||
EXPECT_EQ(this->counter_.state, 3.0f);
|
||||
}
|
||||
|
||||
TEST_F(CounterTest, SourceUpdatesAddToCurrentValue) {
|
||||
sensor::Sensor source;
|
||||
this->counter_.count_updates_from(&source);
|
||||
|
||||
this->counter_.set_value(100);
|
||||
source.publish_state(1.0f);
|
||||
EXPECT_EQ(this->counter_.state, 101.0f);
|
||||
}
|
||||
|
||||
TEST_F(CounterTest, CountsOnlyTrueFromBinarySource) {
|
||||
FakeBinarySource source;
|
||||
this->counter_.count_true_from(&source);
|
||||
|
||||
source.publish(true);
|
||||
source.publish(false);
|
||||
EXPECT_EQ(this->counter_.state, 1.0f);
|
||||
source.publish(true);
|
||||
source.publish(false);
|
||||
source.publish(false);
|
||||
EXPECT_EQ(this->counter_.state, 2.0f);
|
||||
}
|
||||
|
||||
} // namespace esphome::counter::testing
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
counter: !include common.yaml
|
||||
@@ -0,0 +1,12 @@
|
||||
packages:
|
||||
counter: !include common.yaml
|
||||
|
||||
esphome:
|
||||
on_shutdown:
|
||||
then:
|
||||
- counter.set_value:
|
||||
id: counter_no_restore
|
||||
value: -9223372036854775807
|
||||
- counter.increment:
|
||||
id: counter_no_restore
|
||||
value: 9223372036854775807
|
||||
Reference in New Issue
Block a user