From 22536f933833baf7c14482f429de9c2d848a30f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Oct 2026 17:47:06 -0500 Subject: [PATCH] Collapse the range statements into one render time statement --- esphome/components/number/__init__.py | 95 ++++++++++----------------- 1 file changed, 33 insertions(+), 62 deletions(-) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 6326607dd7e..9ea4b536f04 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -1,3 +1,4 @@ +from collections.abc import Callable from dataclasses import dataclass, field from esphome import automation @@ -92,7 +93,7 @@ from esphome.core.entity_helpers import ( setup_unit_of_measurement, ) from esphome.cpp_generator import MockObj, MockObjClass, ProgmemAssignmentExpression -from esphome.types import ConfigType, Expression, SafeExpType +from esphome.types import ConfigType, SafeExpType CODEOWNERS = ["@esphome/core"] DOMAIN = "number" @@ -291,22 +292,10 @@ async def _build_number_automations(var, config): RANGE_TABLE_MIN_USERS = 3 -@dataclass -class NumberRangeUsage: - range_id: ID - values: tuple[float, float, float] - users: int = 0 - - @property - def shared(self) -> bool: - return self.users >= RANGE_TABLE_MIN_USERS - - @dataclass class NumberData: - ranges: dict[tuple[float, float, float], NumberRangeUsage] = field( - default_factory=dict - ) + # (min, max, step) -> range_id of every number using it; the first one names the table. + ranges: dict[tuple[float, float, float], list[ID]] = field(default_factory=dict) def _get_data() -> NumberData: @@ -315,50 +304,16 @@ def _get_data() -> NumberData: return CORE.data[DOMAIN] -class _RangeTableStatement(cg.Statement): - """Global PROGMEM table, rendered only if enough numbers ended up sharing the range.""" +class _RenderTimeStatement(cg.Statement): + """Text chosen when main.cpp is written, after every number has been counted.""" - __slots__ = ("usage",) + __slots__ = ("render",) - def __init__(self, usage: NumberRangeUsage) -> None: - self.usage = usage + def __init__(self, render: Callable[[], str]) -> None: + self.render = render def __str__(self) -> str: - if not self.usage.shared: - return "" - decl = ProgmemAssignmentExpression( - NumberRange, self.usage.range_id, cg.safe_exp([self.usage.values]) - ) - return f"{decl};" - - -class _SetRangeStatement(cg.Statement): - """set_range() on the shared table, or the plain setters, decided once every number is known.""" - - __slots__ = ("var", "usage") - - def __init__(self, var: MockObj, usage: NumberRangeUsage) -> None: - self.var = var - self.usage = usage - - def __str__(self) -> str: - if self.usage.shared: - table = MockObj(self.usage.range_id, ".") - return str(cg.statement(self.var.set_range(table))) - return "\n".join( - str(cg.statement(expr)) - for expr in _range_setters(self.var, *self.usage.values) - ) - - -def _range_setters( - var: MockObj, min_value: SafeExpType, max_value: SafeExpType, step: SafeExpType -) -> tuple[Expression, Expression, Expression]: - return ( - var.traits.set_min_value(min_value), - var.traits.set_max_value(max_value), - var.traits.set_step(step), - ) + return self.render() def _add_range( @@ -368,18 +323,34 @@ def _add_range( max_value: SafeExpType, step: SafeExpType, ) -> None: + setters = ( + var.traits.set_min_value(min_value), + var.traits.set_max_value(max_value), + var.traits.set_step(step), + ) # Lambda bounds (e.g. lvgl arc/bar) are runtime expressions and can't go in a table. if not all(isinstance(v, (int, float)) for v in (min_value, max_value, step)): - for expr in _range_setters(var, min_value, max_value, step): + for expr in setters: cg.add(expr) return key = (float(min_value), float(max_value), float(step)) - ranges = _get_data().ranges - if (usage := ranges.get(key)) is None: - usage = ranges[key] = NumberRangeUsage(range_id, key) - cg.add_global(_RangeTableStatement(usage)) - usage.users += 1 - cg.add(_SetRangeStatement(var, usage)) + users = _get_data().ranges.setdefault(key, []) + users.append(range_id) + table = users[0] + + def render_table() -> str: + if len(users) < RANGE_TABLE_MIN_USERS: + return "" + return f"{ProgmemAssignmentExpression(NumberRange, table, cg.safe_exp([key]))};" + + def render_call() -> str: + if len(users) < RANGE_TABLE_MIN_USERS: + return "\n".join(str(cg.statement(expr)) for expr in setters) + return str(cg.statement(var.set_range(MockObj(table, ".")))) + + if len(users) == 1: + cg.add_global(_RenderTimeStatement(render_table)) + cg.add(_RenderTimeStatement(render_call)) @setup_entity("number")