mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
Size the receiver lists with a keyed slot_counter
cg.slot_counter() now takes an optional key per request and emits the largest count under any one key, so remote_base no longer needs its own counter to give every receiver the same capacity. Add typename to the two dependent ProtocolData parameters.
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -45,8 +44,7 @@ from esphome.const import (
|
||||
CONF_WAND_ID,
|
||||
CONF_ZERO,
|
||||
)
|
||||
from esphome.core import CORE, ID, coroutine, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
from esphome.core import ID, coroutine
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType
|
||||
@@ -103,52 +101,22 @@ REMOTE_TRANSMITTABLE_SCHEMA = cv.Schema(
|
||||
|
||||
# Listener and dumper lists are StaticVectors sized from these counts, so every registration
|
||||
# must go through add_listener / add_dumper. Every receiver's list gets the same capacity, so
|
||||
# the define is the largest count any one receiver needs, not the sum over all receivers.
|
||||
# the slots are keyed by receiver and the define is the largest count any one receiver needs.
|
||||
LISTENER_COUNT_DEFINE = "REMOTE_BASE_LISTENER_COUNT"
|
||||
DUMPER_COUNT_DEFINE = "REMOTE_BASE_DUMPER_COUNT"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _SlotCounts:
|
||||
per_receiver: dict[str, dict[str, int]] = field(default_factory=dict)
|
||||
emitted: bool = False
|
||||
|
||||
|
||||
def _get_slot_counts() -> _SlotCounts:
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = _SlotCounts()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _emit_slot_counts() -> None:
|
||||
state = _get_slot_counts()
|
||||
state.emitted = True
|
||||
for define, counts in state.per_receiver.items():
|
||||
cg.add_define(define, max(counts.values()))
|
||||
|
||||
|
||||
def _request_slot(define: str, receiver: MockObj) -> None:
|
||||
state = _get_slot_counts()
|
||||
if state.emitted:
|
||||
raise ValueError(
|
||||
f"{define}: slot requested after the count define was emitted; "
|
||||
"request slots from to_code, not from a job running after FINAL"
|
||||
)
|
||||
if not state.per_receiver:
|
||||
CORE.add_job(_emit_slot_counts)
|
||||
counts = state.per_receiver.setdefault(define, {})
|
||||
key = str(receiver)
|
||||
counts[key] = counts.get(key, 0) + 1
|
||||
_request_listener_slot = cg.slot_counter(LISTENER_COUNT_DEFINE)
|
||||
_request_dumper_slot = cg.slot_counter(DUMPER_COUNT_DEFINE)
|
||||
|
||||
|
||||
def add_listener(receiver: MockObj, listener: MockObj) -> None:
|
||||
_request_slot(LISTENER_COUNT_DEFINE, receiver)
|
||||
_request_listener_slot(str(receiver))
|
||||
cg.add(receiver.register_listener(listener))
|
||||
|
||||
|
||||
def add_dumper(receiver: MockObj, dumper: MockObj) -> None:
|
||||
_request_slot(DUMPER_COUNT_DEFINE, receiver)
|
||||
_request_dumper_slot(str(receiver))
|
||||
cg.add(receiver.register_dumper(dumper))
|
||||
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ class RemoteTransmitterBase : public RemoteComponentBase {
|
||||
return TransmitCall(this);
|
||||
}
|
||||
template<RemoteProtocolEncoder Protocol>
|
||||
void transmit(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
|
||||
void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
|
||||
auto call = this->transmit();
|
||||
Protocol().encode(call.get_data(), data);
|
||||
call.set_send_times(send_times);
|
||||
@@ -307,7 +307,7 @@ class RemoteTransmittable {
|
||||
|
||||
protected:
|
||||
template<RemoteProtocolEncoder Protocol>
|
||||
void transmit_(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
|
||||
void transmit_(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
|
||||
this->transmitter_->transmit<Protocol>(data, send_times, send_wait);
|
||||
}
|
||||
RemoteTransmitterBase *transmitter_;
|
||||
|
||||
+21
-12
@@ -1,4 +1,4 @@
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Hashable
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
|
||||
@@ -142,9 +142,10 @@ _SLOT_COUNTER_DOMAIN = "slot_counter"
|
||||
|
||||
@dataclass
|
||||
class _SlotCounterState:
|
||||
"""Per-run slot counter state: requested counts and already-emitted defines."""
|
||||
"""Per-run slot counter state: requested counts per define and key, and
|
||||
already-emitted defines."""
|
||||
|
||||
counts: dict[str, int] = field(default_factory=dict)
|
||||
counts: dict[str, dict[Hashable, int]] = field(default_factory=dict)
|
||||
emitted: set[str] = field(default_factory=set)
|
||||
|
||||
|
||||
@@ -156,11 +157,13 @@ def _get_slot_counter_state() -> _SlotCounterState:
|
||||
|
||||
|
||||
def get_slot_count(define: str) -> int:
|
||||
"""Number of slots requested so far for `define`."""
|
||||
return _get_slot_counter_state().counts.get(define, 0)
|
||||
"""Value `define` would be emitted with so far: the largest count requested
|
||||
under any one key, which is the plain request count when no key is used."""
|
||||
counts = _get_slot_counter_state().counts.get(define)
|
||||
return max(counts.values()) if counts else 0
|
||||
|
||||
|
||||
def slot_counter(define: str) -> Callable[[], None]:
|
||||
def slot_counter(define: str) -> Callable[..., None]:
|
||||
"""Create a request_slot function for codegen-sized storage.
|
||||
|
||||
The pattern behind a StaticVector listener array: a consumer's to_code
|
||||
@@ -169,6 +172,11 @@ def slot_counter(define: str) -> Callable[[], None]:
|
||||
emitted with the requested count. No requests, no define: the guarded
|
||||
storage and its registration method compile out entirely.
|
||||
|
||||
When several objects each declare the storage at the same size (one list
|
||||
per receiver, per hub, ...) the caller passes the owning object as `key`
|
||||
and the define becomes the largest count any one key requested, not the
|
||||
total. Requests without a key share one count.
|
||||
|
||||
The counts live in a table under CORE.data, which clears between runs.
|
||||
A request arriving after the define was already emitted raises instead of
|
||||
silently undercounting: the define would keep the stale smaller value and
|
||||
@@ -179,10 +187,10 @@ def slot_counter(define: str) -> Callable[[], None]:
|
||||
async def emit_job() -> None:
|
||||
state = _get_slot_counter_state()
|
||||
state.emitted.add(define)
|
||||
# Scheduled only by the first request, so the count is always >= 1 here.
|
||||
add_define(define, state.counts[define])
|
||||
# Scheduled only by the first request, so there is at least one count here.
|
||||
add_define(define, max(state.counts[define].values()))
|
||||
|
||||
def request_slot() -> None:
|
||||
def request_slot(key: Hashable = None) -> None:
|
||||
state = _get_slot_counter_state()
|
||||
if define in state.emitted:
|
||||
raise ValueError(
|
||||
@@ -190,10 +198,11 @@ def slot_counter(define: str) -> Callable[[], None]:
|
||||
f"define was emitted; request slots from to_code, not from a "
|
||||
f"job running after FINAL emission"
|
||||
)
|
||||
counts = state.counts
|
||||
counts[define] = (count := counts.get(define, 0) + 1)
|
||||
if count == 1:
|
||||
counts = state.counts.get(define)
|
||||
if counts is None:
|
||||
counts = state.counts[define] = {}
|
||||
CORE.add_job(emit_job)
|
||||
counts[key] = counts.get(key, 0) + 1
|
||||
|
||||
return request_slot
|
||||
|
||||
|
||||
@@ -187,6 +187,19 @@ def test_slot_counter_emits_requested_count() -> None:
|
||||
assert _define_value("TEST_SLOT_COUNT") == "2"
|
||||
|
||||
|
||||
def test_slot_counter_keyed_emits_largest_count() -> None:
|
||||
"""Keyed requests size storage every key declares at the same capacity:
|
||||
the define is the busiest key's count, not the total over all keys."""
|
||||
request = ch.slot_counter("TEST_SLOT_COUNT_KEYED")
|
||||
request("rx_a")
|
||||
request("rx_a")
|
||||
request("rx_a")
|
||||
request("rx_b")
|
||||
assert ch.get_slot_count("TEST_SLOT_COUNT_KEYED") == 3
|
||||
ch.CORE.flush_tasks()
|
||||
assert _define_value("TEST_SLOT_COUNT_KEYED") == "3"
|
||||
|
||||
|
||||
def test_slot_counter_without_requests_emits_nothing() -> None:
|
||||
"""No requests, no job, no define — the guarded storage compiles out."""
|
||||
ch.slot_counter("TEST_SLOT_COUNT_UNUSED")
|
||||
|
||||
Reference in New Issue
Block a user