mirror of
https://github.com/esphome/esphome.git
synced 2026-10-24 15:05:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26a5ca2483 | ||
|
|
de83f63d3c | ||
|
|
24cf95fc53 | ||
|
|
487c133831 | ||
|
|
0a3157ed21 |
No files matched your search
@@ -449,8 +449,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
```
|
||||
Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `restore(default)`
|
||||
to still drive a state when the restore mode is disabled.
|
||||
A hub that applies its switches' restored states itself reads `sw->get_initial_write_state()`, which
|
||||
already applies `inverted`.
|
||||
|
||||
- **Other codegen helpers:** `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`,
|
||||
`spi.new_spi_component`, `modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`)
|
||||
|
||||
@@ -173,8 +173,8 @@ void DFRobotSEN0609Component::setup() {
|
||||
switch_::Switch *sw = this->switches_[i];
|
||||
if (sw == nullptr)
|
||||
continue;
|
||||
// A disabled restore mode leaves the switch on, which an inverted switch writes as off
|
||||
this->publish_switch_(static_cast<SwitchSetting>(i), sw->get_initial_write_state().value_or(!sw->is_inverted()));
|
||||
const bool on = sw->get_initial_state_with_restore_mode().value_or(true);
|
||||
this->publish_switch_(static_cast<SwitchSetting>(i), on != sw->is_inverted());
|
||||
}
|
||||
#endif
|
||||
this->queue_configuration_();
|
||||
|
||||
@@ -155,9 +155,12 @@ void HitachiClimate::setup() {
|
||||
set_swing_v_(swing_vertical(restore->swing_mode));
|
||||
}
|
||||
#ifdef USE_SWITCH
|
||||
// The restored value is the switch state, which an inverted switch flips relative to the frame bit
|
||||
if (this->mildew_proof_switch_ != nullptr) {
|
||||
if (auto initial = this->mildew_proof_switch_->get_initial_write_state())
|
||||
assign_bits(this->remote_state_[HITACHI_AC344_MILDEWPROOF_BYTE], 1 << HITACHI_AC344_MILDEWPROOF_OFFSET, *initial);
|
||||
if (auto initial = this->mildew_proof_switch_->get_initial_state_with_restore_mode()) {
|
||||
assign_bits(this->remote_state_[HITACHI_AC344_MILDEWPROOF_BYTE], 1 << HITACHI_AC344_MILDEWPROOF_OFFSET,
|
||||
*initial != this->mildew_proof_switch_->is_inverted());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
this->setup_(restore);
|
||||
|
||||
@@ -25,7 +25,7 @@ void SeeedMultiChannelRelay::setup() {
|
||||
switch_::Switch *sw = this->switches_[channel];
|
||||
if (sw == nullptr)
|
||||
continue;
|
||||
const bool on = sw->get_initial_write_state().value_or(false);
|
||||
const bool on = sw->get_initial_state_with_restore_mode().value_or(sw->is_inverted()) != sw->is_inverted();
|
||||
assign_bits(value, 1 << channel, on);
|
||||
}
|
||||
if (!this->status_comm_result(this->write_byte(CMD_CHANNEL_CTRL, value)))
|
||||
|
||||
@@ -60,11 +60,6 @@ optional<bool> Switch::get_initial_state_with_restore_mode() {
|
||||
}
|
||||
return initial_state;
|
||||
}
|
||||
optional<bool> Switch::get_initial_write_state() {
|
||||
if (auto initial = this->get_initial_state_with_restore_mode())
|
||||
return *initial != this->inverted_;
|
||||
return {};
|
||||
}
|
||||
void Switch::restore() {
|
||||
if (auto initial = this->get_initial_state_with_restore_mode())
|
||||
this->control(*initial);
|
||||
|
||||
@@ -114,8 +114,6 @@ class Switch : public EntityBase {
|
||||
* state.
|
||||
*/
|
||||
optional<bool> get_initial_state_with_restore_mode();
|
||||
/// get_initial_state_with_restore_mode() with inverted applied: the level write_state() takes.
|
||||
optional<bool> get_initial_write_state();
|
||||
|
||||
/// Drive the switch to the state its restore mode yields, through control(), so inversion and
|
||||
/// write_state() apply; a disabled restore mode drives \p when_disabled, or nothing without one.
|
||||
|
||||
@@ -46,16 +46,4 @@ TEST(SwitchRestore, DefaultAppliesOnlyWhenTheModeIsDisabled) {
|
||||
EXPECT_EQ(on.writes, std::vector<bool>{true});
|
||||
}
|
||||
|
||||
TEST(SwitchRestore, InitialWriteStateAppliesInversion) {
|
||||
RecordingSwitch on, inverted, disabled;
|
||||
on.set_restore_mode(SWITCH_ALWAYS_ON);
|
||||
inverted.set_restore_mode(SWITCH_ALWAYS_ON);
|
||||
inverted.set_inverted(true);
|
||||
disabled.set_restore_mode(SWITCH_RESTORE_DISABLED);
|
||||
disabled.set_inverted(true);
|
||||
EXPECT_EQ(on.get_initial_write_state(), optional<bool>(true));
|
||||
EXPECT_EQ(inverted.get_initial_write_state(), optional<bool>(false));
|
||||
EXPECT_FALSE(disabled.get_initial_write_state().has_value());
|
||||
}
|
||||
|
||||
} // namespace esphome::switch_::testing
|
||||
@@ -595,8 +595,8 @@ class SensorStateCollector:
|
||||
class SensorTracker:
|
||||
"""Data-driven sensor state tracker with expected-value futures.
|
||||
|
||||
Tracks sensor and binary sensor state updates and resolves futures when
|
||||
they report specific expected values. Eliminates per-sensor future
|
||||
Tracks sensor, binary sensor and text sensor state updates and resolves
|
||||
futures when they report specific expected values. Eliminates per-sensor future
|
||||
boilerplate.
|
||||
|
||||
Usage::
|
||||
@@ -608,12 +608,20 @@ class SensorTracker:
|
||||
"""
|
||||
|
||||
def __init__(self, sensor_names: list[str]) -> None:
|
||||
self.sensor_states: dict[str, list[float]] = {name: [] for name in sensor_names}
|
||||
self.sensor_states: dict[str, list[float | bool | str]] = {
|
||||
name: [] for name in sensor_names
|
||||
}
|
||||
self.key_to_sensor: dict[int, str] = {}
|
||||
self._expectations: dict[str, list[tuple[object, asyncio.Future]]] = {}
|
||||
|
||||
_ANY = object() # Sentinel: match any value
|
||||
|
||||
def map_entities(self, entities: list[EntityInfo]) -> None:
|
||||
"""Map the tracked names to the keys of ``entities``."""
|
||||
self.key_to_sensor.update(
|
||||
build_key_to_entity_mapping(entities, list(self.sensor_states))
|
||||
)
|
||||
|
||||
def expect(self, name: str, value: object) -> asyncio.Future:
|
||||
"""Register an expected value for *name* and return a future for it."""
|
||||
future: asyncio.Future = asyncio.get_running_loop().create_future()
|
||||
@@ -638,7 +646,7 @@ class SensorTracker:
|
||||
connect-time states so they cannot satisfy a later phase.
|
||||
"""
|
||||
if (
|
||||
not isinstance(state, (SensorState, BinarySensorState))
|
||||
not isinstance(state, (SensorState, BinarySensorState, TextSensorState))
|
||||
or state.missing_state
|
||||
):
|
||||
return
|
||||
@@ -708,9 +716,7 @@ class SensorTracker:
|
||||
satisfy a later phase.
|
||||
"""
|
||||
entities, _ = await client.list_entities_services()
|
||||
self.key_to_sensor.update(
|
||||
build_key_to_entity_mapping(entities, list(self.sensor_states.keys()))
|
||||
)
|
||||
self.map_entities(entities)
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(self.on_state))
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
|
||||
@@ -6,7 +6,7 @@ from aioesphomeapi import connection as api_connection
|
||||
from aioesphomeapi.api_pb2 import GetTimeResponse
|
||||
import pytest
|
||||
|
||||
from .state_utils import SensorTracker, build_key_to_entity_mapping
|
||||
from .state_utils import SensorTracker
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# 2024-01-01 00:00:00 UTC
|
||||
@@ -32,7 +32,7 @@ async def test_api_get_time_response_timezone(
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
tracker = SensorTracker(["tz_offset"])
|
||||
tracker.key_to_sensor = build_key_to_entity_mapping(entities, ["tz_offset"])
|
||||
tracker.map_entities(entities)
|
||||
client.subscribe_states(tracker.on_state)
|
||||
|
||||
await tracker.await_change(tracker.expect_any("tz_offset"), "tz_offset")
|
||||
|
||||
@@ -19,11 +19,7 @@ import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from .state_utils import (
|
||||
SensorTracker,
|
||||
build_key_to_entity_mapping,
|
||||
subscribe_after_initial_states,
|
||||
)
|
||||
from .state_utils import SensorTracker, subscribe_after_initial_states
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# Time to let the host-mode main loop run so status_led_light.loop() can
|
||||
@@ -67,9 +63,7 @@ async def test_status_flags(
|
||||
"status_led_last_state",
|
||||
]
|
||||
)
|
||||
tracker.key_to_sensor.update(
|
||||
build_key_to_entity_mapping(entities, list(tracker.sensor_states.keys()))
|
||||
)
|
||||
tracker.map_entities(entities)
|
||||
|
||||
# Swallow initial state broadcasts so the test only reacts to
|
||||
# state changes triggered by our service calls.
|
||||
|
||||
@@ -2,14 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import EntityState, TextSensorState
|
||||
import pytest
|
||||
|
||||
from .state_utils import build_key_to_entity_mapping, subscribe_after_initial_states
|
||||
from .state_utils import SensorTracker, subscribe_after_initial_states
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
from .wait_utils import wait_or_fail
|
||||
|
||||
# Timestamps are exposed as text sensors (rather than 32-bit float sensor states,
|
||||
# which cannot represent a UNIX epoch exactly) so the exact value can be checked.
|
||||
@@ -30,18 +26,10 @@ async def test_template_time(
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
key_to_name = build_key_to_entity_mapping(entities, list(EXPECTED_STATES))
|
||||
events = {name: asyncio.Event() for name in EXPECTED_STATES}
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if isinstance(state, TextSensorState) and not state.missing_state:
|
||||
name = key_to_name.get(state.key)
|
||||
if name is not None and state.state == EXPECTED_STATES[name]:
|
||||
events[name].set()
|
||||
|
||||
await subscribe_after_initial_states(client, entities, on_state=on_state)
|
||||
|
||||
for name, event in events.items():
|
||||
await wait_or_fail(
|
||||
event, 3.0, f"Timeout waiting for {name} to report its expected value"
|
||||
)
|
||||
tracker = SensorTracker(list(EXPECTED_STATES))
|
||||
tracker.map_entities(entities)
|
||||
futures = tracker.expect_all(EXPECTED_STATES)
|
||||
await subscribe_after_initial_states(
|
||||
client, entities, on_state=tracker.on_state
|
||||
)
|
||||
await tracker.await_all(futures, timeout=3.0)
|
||||
Reference in new issue
Block a user