Share the entity unit lookup and reject unknown temperature units

This commit is contained in:
J. Nick Koston
2026-10-13 21:01:57 -10:00
parent fb04ec0928
commit 6a0e0358f8
7 changed files with 32 additions and 41 deletions
+5 -18
View File
@@ -49,16 +49,16 @@ from esphome.const import (
CONF_UNIT_OF_MEASUREMENT,
CONF_VISUAL,
CONF_WEB_SERVER,
UNIT_CELSIUS,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
entity_temperature_unit,
entity_temperature_unit_of_id,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.cpp_types import TEMPERATURE_UNITS
from esphome.types import ConfigType, SafeExpType
IS_PLATFORM_COMPONENT = True
@@ -158,30 +158,17 @@ def visual_temperature_step(value: Any) -> ConfigType:
return {CONF_TARGET_TEMPERATURE: value, CONF_CURRENT_TEMPERATURE: value}
def temperature_unit_of(config: ConfigType) -> str:
"""The unit a climate reports in: ``unit_of_measurement`` when the platform took the option
through ``climate_schema``, else Celsius (anova has an unrelated option of that name)."""
unit = config.get(CONF_UNIT_OF_MEASUREMENT)
return unit if unit in TEMPERATURE_UNITS else UNIT_CELSIUS
def temperature_unit_of_id(climate_id: ID) -> str:
"""The unit of the climate with this id, for actions that take static temperatures."""
path = CORE.config.get_path_for_id(climate_id)[:-1]
return temperature_unit_of(CORE.config.get_config_for_path(path))
def temperature_in_entity_unit(
config: ConfigType, value: tuple[float, str | None]
) -> str:
"""Render a static action temperature in the unit of the climate the action targets."""
unit = temperature_unit_of_id(config[CONF_ID])
unit = entity_temperature_unit_of_id(config[CONF_ID])
return str(cg.safe_exp(cv.convert_temperature(*value, unit)))
def _visual_temperatures_in_entity_unit(config: ConfigType) -> ConfigType:
"""Convert the visual temperatures into the entity's unit."""
unit = temperature_unit_of(config)
unit = entity_temperature_unit(config)
visual = config[CONF_VISUAL]
for key in (CONF_MIN_TEMPERATURE, CONF_MAX_TEMPERATURE):
if isinstance(value := visual.get(key), tuple):
@@ -29,6 +29,7 @@ from esphome.const import (
CONF_TARGET_TEMPERATURE_LOW,
CONF_UNIT_OF_MEASUREMENT,
)
from esphome.core.entity_helpers import entity_temperature_unit
from esphome.cpp_types import TEMPERATURE_UNITS
from esphome.types import ConfigType
@@ -200,7 +201,7 @@ def _validate_initial_state(config: ConfigType) -> ConfigType:
f"'{CONF_TARGET_HUMIDITY}' requires '{CONF_SUPPORTS_TARGET_HUMIDITY}' to be enabled",
path=[CONF_INITIAL_STATE, CONF_TARGET_HUMIDITY],
)
unit = climate.temperature_unit_of(config)
unit = entity_temperature_unit(config)
for key in TEMPERATURE_KEYS:
if isinstance(value := initial_state.get(key), tuple):
initial_state[key] = cv.convert_temperature(*value, unit)
@@ -15,6 +15,7 @@ from esphome.const import (
CONF_UNIT_OF_MEASUREMENT,
)
from esphome.core import ID
from esphome.core.entity_helpers import entity_temperature_unit_of_id
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.cpp_types import TEMPERATURE_UNITS
from esphome.types import ConfigType
@@ -154,7 +155,7 @@ async def water_heater_template_publish_to_code(
await cg.register_parented(var, config[CONF_ID])
# A static temperature is in the unit of the water heater it is published to
unit = water_heater.temperature_unit_of_id(config[CONF_ID])
unit = entity_temperature_unit_of_id(config[CONF_ID])
for key, setter in (
(CONF_CURRENT_TEMPERATURE, var.set_current_temperature),
(CONF_TARGET_TEMPERATURE, var.set_target_temperature),
+3 -17
View File
@@ -8,16 +8,15 @@ from esphome.const import (
CONF_MIN_TEMPERATURE,
CONF_UNIT_OF_MEASUREMENT,
CONF_VISUAL,
UNIT_CELSIUS,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
entity_temperature_unit,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
from esphome.cpp_types import TEMPERATURE_UNITS
from esphome.types import ConfigType
CODEOWNERS = ["@dhoeben"]
@@ -45,22 +44,9 @@ WATER_HEATER_MODES = {
validate_water_heater_mode = cv.enum(WATER_HEATER_MODES, upper=True)
def temperature_unit_of(config: ConfigType) -> str:
"""The unit a water heater reports in: ``unit_of_measurement`` when the platform took the
option through ``water_heater_schema``, else Celsius."""
unit = config.get(CONF_UNIT_OF_MEASUREMENT)
return unit if unit in TEMPERATURE_UNITS else UNIT_CELSIUS
def temperature_unit_of_id(water_heater_id: ID) -> str:
"""The unit of the water heater with this id, for actions that take static temperatures."""
path = CORE.config.get_path_for_id(water_heater_id)[:-1]
return temperature_unit_of(CORE.config.get_config_for_path(path))
def _visual_temperatures_in_entity_unit(config: ConfigType) -> ConfigType:
"""Convert the visual temperatures into the entity's unit."""
unit = temperature_unit_of(config)
unit = entity_temperature_unit(config)
visual = config[CONF_VISUAL]
for key in (CONF_MIN_TEMPERATURE, CONF_MAX_TEMPERATURE):
if isinstance(value := visual.get(key), tuple):
+2
View File
@@ -1472,6 +1472,8 @@ def _temperature_to_celsius(value: float, unit: str, delta: bool) -> float:
return value * (5 / 9) if delta else (value - 32) * (5 / 9)
if unit == UNIT_KELVIN:
return value if delta else value - 273.15
if unit != UNIT_CELSIUS:
raise ValueError(f"Unknown temperature unit {unit!r}")
return value
+15 -1
View File
@@ -16,6 +16,7 @@ from esphome.const import (
CONF_INTERNAL,
CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
UNIT_CELSIUS,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.config import (
@@ -24,7 +25,7 @@ from esphome.core.config import (
UNIT_OF_MEASUREMENT_MAX_LENGTH,
)
from esphome.cpp_generator import MockObj, RawStatement, add, get_variable
from esphome.cpp_types import App
from esphome.cpp_types import TEMPERATURE_UNITS, App
import esphome.final_validate as fv
from esphome.helpers import (
cpp_string_escape,
@@ -550,6 +551,19 @@ def inherit_property_from(property_to_inherit, parent_id_property, transform=Non
return inherit_property
def entity_temperature_unit(config: ConfigType) -> str:
"""The unit a climate or water heater reports in: ``unit_of_measurement`` when the platform took
the option through its schema factory, else Celsius (anova has an unrelated option of that name)."""
unit = config.get(CONF_UNIT_OF_MEASUREMENT)
return unit if unit in TEMPERATURE_UNITS else UNIT_CELSIUS
def entity_temperature_unit_of_id(entity_id: ID) -> str:
"""The unit of the entity with this id, for actions that take static temperatures."""
path = CORE.config.get_path_for_id(entity_id)[:-1]
return entity_temperature_unit(CORE.config.get_config_for_path(path))
def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigType]:
"""Create a validator function to check for duplicate entity names.
@@ -4,7 +4,6 @@ import pytest
from esphome.components.climate import (
_visual_temperatures_in_entity_unit,
temperature_unit_of,
visual_temperature_step,
)
from esphome.const import (
@@ -18,6 +17,7 @@ from esphome.const import (
UNIT_CELSIUS,
UNIT_FAHRENHEIT,
)
from esphome.core.entity_helpers import entity_temperature_unit
def test_visual_temperature_step_scalar_applies_to_both() -> None:
@@ -67,9 +67,9 @@ def test_visual_temperatures_follow_unit_of_measurement() -> None:
def test_platform_with_its_own_unit_option_keeps_celsius() -> None:
# anova validates unit_of_measurement as c|f for the device, not for the entity
assert temperature_unit_of({CONF_UNIT_OF_MEASUREMENT: "f"}) == UNIT_CELSIUS
assert entity_temperature_unit({CONF_UNIT_OF_MEASUREMENT: "f"}) == UNIT_CELSIUS
assert (
temperature_unit_of({CONF_UNIT_OF_MEASUREMENT: UNIT_FAHRENHEIT})
entity_temperature_unit({CONF_UNIT_OF_MEASUREMENT: UNIT_FAHRENHEIT})
== UNIT_FAHRENHEIT
)