mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 22:06:12 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a0e0358f8 | ||
|
|
fb04ec0928 | ||
|
|
08c1659d5b | ||
|
|
41dad01e6e | ||
|
|
34f91e79b5 | ||
|
|
de1e56cd15 | ||
|
|
f9a01bef46 |
@@ -338,8 +338,9 @@ class ApplyField:
|
||||
Double a literal brace in a template. ``conf_key`` may be a path into nested sections.
|
||||
``type_`` may be a C++ type string using ``{parent}`` when the type is only known per
|
||||
instance. ``const_fn(config, value)`` renders a constant's argument text when ``cg.safe_exp``
|
||||
is not the right spelling (unit conversion belongs in the validator); a lambda or an id
|
||||
bypasses it, so the target must also take a plain ``type_``. An absent key emits nothing.
|
||||
is not the right spelling, or the value depends on another entity's config, such as the unit
|
||||
a climate reports in; a lambda or an id bypasses it, so the target must also take a plain
|
||||
``type_``. An absent key emits nothing.
|
||||
"""
|
||||
|
||||
conf_key: str | tuple[str, ...]
|
||||
|
||||
@@ -46,12 +46,15 @@ from esphome.const import (
|
||||
CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC,
|
||||
CONF_TARGET_TEMPERATURE_STATE_TOPIC,
|
||||
CONF_TEMPERATURE_STEP,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VISUAL,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
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,
|
||||
)
|
||||
@@ -137,13 +140,10 @@ CONF_MIN_HUMIDITY = "min_humidity"
|
||||
CONF_MAX_HUMIDITY = "max_humidity"
|
||||
CONF_TARGET_HUMIDITY = "target_humidity"
|
||||
|
||||
visual_temperature = cv.float_with_unit("visual_temperature", "(°|(° ?)?[CKF])?")
|
||||
|
||||
|
||||
VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_TARGET_TEMPERATURE): visual_temperature,
|
||||
cv.Required(CONF_CURRENT_TEMPERATURE): visual_temperature,
|
||||
cv.Required(CONF_TARGET_TEMPERATURE): cv.temperature_with_unit,
|
||||
cv.Required(CONF_CURRENT_TEMPERATURE): cv.temperature_with_unit,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -154,13 +154,30 @@ def visual_temperature_step(value: Any) -> ConfigType:
|
||||
return VISUAL_TEMPERATURE_STEP_SCHEMA(value)
|
||||
|
||||
# Otherwise, use the single value for both properties
|
||||
value = visual_temperature(value)
|
||||
return VISUAL_TEMPERATURE_STEP_SCHEMA(
|
||||
{
|
||||
CONF_TARGET_TEMPERATURE: value,
|
||||
CONF_CURRENT_TEMPERATURE: value,
|
||||
}
|
||||
)
|
||||
value = cv.temperature_with_unit(value)
|
||||
return {CONF_TARGET_TEMPERATURE: value, CONF_CURRENT_TEMPERATURE: value}
|
||||
|
||||
|
||||
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 = 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 = entity_temperature_unit(config)
|
||||
visual = config[CONF_VISUAL]
|
||||
for key in (CONF_MIN_TEMPERATURE, CONF_MAX_TEMPERATURE):
|
||||
if isinstance(value := visual.get(key), tuple):
|
||||
visual[key] = cv.convert_temperature(*value, unit)
|
||||
if (step := visual.get(CONF_TEMPERATURE_STEP)) is not None:
|
||||
for key in (CONF_TARGET_TEMPERATURE, CONF_CURRENT_TEMPERATURE):
|
||||
if isinstance(value := step[key], tuple):
|
||||
step[key] = cv.convert_temperature(*value, unit, delta=True)
|
||||
return config
|
||||
|
||||
|
||||
_CLIMATE_SCHEMA = (
|
||||
@@ -171,8 +188,8 @@ _CLIMATE_SCHEMA = (
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent),
|
||||
cv.Optional(CONF_VISUAL, default={}): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
|
||||
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
|
||||
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_TEMPERATURE_STEP): visual_temperature_step,
|
||||
cv.Optional(CONF_MIN_HUMIDITY): cv.percentage_int,
|
||||
cv.Optional(CONF_MAX_HUMIDITY): cv.percentage_int,
|
||||
@@ -249,6 +266,7 @@ _CLIMATE_SCHEMA = (
|
||||
|
||||
|
||||
_CLIMATE_SCHEMA.add_extra(entity_duplicate_validator("climate"))
|
||||
_CLIMATE_SCHEMA.add_extra(_visual_temperatures_in_entity_unit)
|
||||
|
||||
|
||||
def climate_schema(
|
||||
@@ -256,10 +274,18 @@ def climate_schema(
|
||||
*,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
icon: str = cv.UNDEFINED,
|
||||
temperature_unit: bool = False,
|
||||
) -> cv.Schema:
|
||||
"""Build the schema of a climate platform.
|
||||
|
||||
``temperature_unit`` adds ``unit_of_measurement`` for platforms whose temperatures are all
|
||||
in the unit the user picks; the class then needs ``set_temperature_unit(TemperatureUnit)``.
|
||||
"""
|
||||
schema = {
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
}
|
||||
if temperature_unit:
|
||||
schema[cv.Optional(CONF_UNIT_OF_MEASUREMENT)] = cv.temperature_unit
|
||||
|
||||
for key, default, validator in [
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
@@ -460,9 +486,13 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Climate),
|
||||
cv.Optional(CONF_MODE): cv.templatable(validate_climate_mode),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature_with_unit),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int),
|
||||
cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"),
|
||||
cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
|
||||
@@ -482,12 +512,23 @@ automation.register_apply_action(
|
||||
"climate.control",
|
||||
CLIMATE_CONTROL_ACTION_SCHEMA,
|
||||
automation.ApplyField(CONF_MODE, "set_mode", ClimateMode),
|
||||
automation.ApplyField(CONF_TARGET_TEMPERATURE, "set_target_temperature", cg.float_),
|
||||
automation.ApplyField(
|
||||
CONF_TARGET_TEMPERATURE_LOW, "set_target_temperature_low", cg.float_
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
"set_target_temperature",
|
||||
cg.float_,
|
||||
temperature_in_entity_unit,
|
||||
),
|
||||
automation.ApplyField(
|
||||
CONF_TARGET_TEMPERATURE_HIGH, "set_target_temperature_high", cg.float_
|
||||
CONF_TARGET_TEMPERATURE_LOW,
|
||||
"set_target_temperature_low",
|
||||
cg.float_,
|
||||
temperature_in_entity_unit,
|
||||
),
|
||||
automation.ApplyField(
|
||||
CONF_TARGET_TEMPERATURE_HIGH,
|
||||
"set_target_temperature_high",
|
||||
cg.float_,
|
||||
temperature_in_entity_unit,
|
||||
),
|
||||
automation.ApplyField(CONF_TARGET_HUMIDITY, "set_target_humidity", cg.float_),
|
||||
automation.ApplyField(CONF_FAN_MODE, "set_fan_mode", ClimateFanMode),
|
||||
|
||||
@@ -27,7 +27,10 @@ from esphome.const import (
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
CONF_TARGET_TEMPERATURE_HIGH,
|
||||
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
|
||||
|
||||
from .. import template_ns
|
||||
@@ -162,6 +165,13 @@ def _resolve_supports(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
TEMPERATURE_KEYS = (
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
CONF_TARGET_TEMPERATURE_LOW,
|
||||
CONF_TARGET_TEMPERATURE_HIGH,
|
||||
)
|
||||
|
||||
|
||||
def _validate_initial_state(config: ConfigType) -> ConfigType:
|
||||
# Climate keeps target_temperature and target_temperature_low in a union, so writing the wrong
|
||||
# one of the pair corrupts the setpoint with no runtime complaint.
|
||||
@@ -191,6 +201,10 @@ 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 = 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)
|
||||
return config
|
||||
|
||||
|
||||
@@ -200,9 +214,9 @@ INITIAL_STATE_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MODE): climate.validate_climate_mode,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.temperature,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.temperature,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.temperature,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_TARGET_HUMIDITY): cv.percentage_int,
|
||||
cv.Exclusive(CONF_FAN_MODE, "fan_mode"): climate.validate_climate_fan_mode,
|
||||
cv.Exclusive(
|
||||
@@ -217,7 +231,7 @@ INITIAL_STATE_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
climate.climate_schema(TemplateClimate)
|
||||
climate.climate_schema(TemplateClimate, temperature_unit=True)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
|
||||
@@ -295,6 +309,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await climate.register_climate(var, config)
|
||||
if (unit := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None:
|
||||
cg.add(var.set_temperature_unit(TEMPERATURE_UNITS[unit]))
|
||||
|
||||
if (sens := config.get(CONF_SENSOR)) is not None:
|
||||
cg.add(var.set_sensor(await cg.get_variable(sens)))
|
||||
@@ -381,11 +397,19 @@ CLIMATE_TEMPLATE_PUBLISH_ACTION_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(TemplateClimate),
|
||||
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_CURRENT_HUMIDITY): cv.templatable(cv.percentage_int),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int),
|
||||
cv.Optional(CONF_MODE): cv.templatable(climate.validate_climate_mode),
|
||||
cv.Optional(CONF_ACTION): cv.templatable(climate.validate_climate_action),
|
||||
@@ -414,15 +438,29 @@ automation.register_apply_action(
|
||||
"climate.template.publish",
|
||||
CLIMATE_TEMPLATE_PUBLISH_ACTION_SCHEMA,
|
||||
automation.ApplyField(
|
||||
CONF_CURRENT_TEMPERATURE, "current_temperature = {}", cg.float_
|
||||
CONF_CURRENT_TEMPERATURE,
|
||||
"current_temperature = {}",
|
||||
cg.float_,
|
||||
climate.temperature_in_entity_unit,
|
||||
),
|
||||
automation.ApplyField(CONF_CURRENT_HUMIDITY, "current_humidity = {}", cg.float_),
|
||||
automation.ApplyField(CONF_TARGET_TEMPERATURE, "set_target_temperature", cg.float_),
|
||||
automation.ApplyField(
|
||||
CONF_TARGET_TEMPERATURE_LOW, "set_target_temperature_low", cg.float_
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
"set_target_temperature",
|
||||
cg.float_,
|
||||
climate.temperature_in_entity_unit,
|
||||
),
|
||||
automation.ApplyField(
|
||||
CONF_TARGET_TEMPERATURE_HIGH, "set_target_temperature_high", cg.float_
|
||||
CONF_TARGET_TEMPERATURE_LOW,
|
||||
"set_target_temperature_low",
|
||||
cg.float_,
|
||||
climate.temperature_in_entity_unit,
|
||||
),
|
||||
automation.ApplyField(
|
||||
CONF_TARGET_TEMPERATURE_HIGH,
|
||||
"set_target_temperature_high",
|
||||
cg.float_,
|
||||
climate.temperature_in_entity_unit,
|
||||
),
|
||||
automation.ApplyField(CONF_TARGET_HUMIDITY, "set_target_humidity", cg.float_),
|
||||
automation.ApplyField(CONF_MODE, "set_mode", climate.ClimateMode),
|
||||
|
||||
@@ -22,6 +22,7 @@ class TemplateClimate final : public climate::Climate, public Component {
|
||||
climate::ClimateTraits traits() override { return this->traits_; }
|
||||
|
||||
void add_feature_flags(uint32_t flags) { this->traits_.add_feature_flags(flags); }
|
||||
void set_temperature_unit(TemperatureUnit unit) { this->traits_.set_temperature_unit(unit); }
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// The matching feature flag is added from codegen, so the configuration alone decides it.
|
||||
|
||||
@@ -12,9 +12,12 @@ from esphome.const import (
|
||||
CONF_SET_ACTION,
|
||||
CONF_SUPPORTED_MODES,
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
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
|
||||
|
||||
from .. import template_ns
|
||||
@@ -39,7 +42,7 @@ RESTORE_MODES = {
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
water_heater.water_heater_schema(TemplateWaterHeater)
|
||||
water_heater.water_heater_schema(TemplateWaterHeater, temperature_unit=True)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean,
|
||||
@@ -65,6 +68,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await water_heater.register_water_heater(var, config)
|
||||
if (unit := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None:
|
||||
cg.add(var.set_temperature_unit(TEMPERATURE_UNITS[unit]))
|
||||
|
||||
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
|
||||
|
||||
@@ -125,8 +130,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(TemplateWaterHeater),
|
||||
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_MODE): cv.templatable(
|
||||
water_heater.validate_water_heater_mode
|
||||
),
|
||||
@@ -145,13 +154,17 @@ async def water_heater_template_publish_to_code(
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
if current_temp := config.get(CONF_CURRENT_TEMPERATURE):
|
||||
template_ = await cg.templatable(current_temp, args, cg.float_)
|
||||
cg.add(var.set_current_temperature(template_))
|
||||
|
||||
if target_temp := config.get(CONF_TARGET_TEMPERATURE):
|
||||
template_ = await cg.templatable(target_temp, args, cg.float_)
|
||||
cg.add(var.set_target_temperature(template_))
|
||||
# A static temperature is in the unit of the water heater it is published to
|
||||
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),
|
||||
):
|
||||
if (value := config.get(key)) is None:
|
||||
continue
|
||||
if isinstance(value, tuple):
|
||||
value = cv.convert_temperature(*value, unit)
|
||||
cg.add(setter(await cg.templatable(value, args, cg.float_)))
|
||||
|
||||
if mode := config.get(CONF_MODE):
|
||||
template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode)
|
||||
|
||||
@@ -32,6 +32,7 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
|
||||
}
|
||||
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_temperature_unit(this->temperature_unit_);
|
||||
if (this->target_temperature_f_.has_value()) {
|
||||
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class TemplateWaterHeater final : public Component, public water_heater::WaterHe
|
||||
|
||||
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
void set_temperature_unit(TemperatureUnit unit) { this->temperature_unit_ = unit; }
|
||||
void set_supported_modes(const std::initializer_list<water_heater::WaterHeaterMode> &modes) {
|
||||
this->supported_modes_ = modes;
|
||||
}
|
||||
@@ -54,6 +55,7 @@ class TemplateWaterHeater final : public Component, public water_heater::WaterHe
|
||||
TemplateLambda<bool> away_f_;
|
||||
TemplateLambda<bool> is_on_f_;
|
||||
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
|
||||
TemperatureUnit temperature_unit_{TemperatureUnit::CELSIUS};
|
||||
water_heater::WaterHeaterModeMask supported_modes_;
|
||||
bool optimistic_{true};
|
||||
};
|
||||
|
||||
@@ -6,11 +6,13 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MAX_TEMPERATURE,
|
||||
CONF_MIN_TEMPERATURE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VISUAL,
|
||||
)
|
||||
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,
|
||||
)
|
||||
@@ -41,19 +43,35 @@ WATER_HEATER_MODES = {
|
||||
}
|
||||
validate_water_heater_mode = cv.enum(WATER_HEATER_MODES, upper=True)
|
||||
|
||||
|
||||
def _visual_temperatures_in_entity_unit(config: ConfigType) -> ConfigType:
|
||||
"""Convert the visual temperatures into the entity's unit."""
|
||||
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):
|
||||
visual[key] = cv.convert_temperature(*value, unit)
|
||||
if isinstance(value := visual.get(CONF_TARGET_TEMPERATURE_STEP), tuple):
|
||||
visual[CONF_TARGET_TEMPERATURE_STEP] = cv.convert_temperature(
|
||||
*value, unit, delta=True
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
_WATER_HEATER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_VISUAL, default={}): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
|
||||
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_STEP): cv.float_,
|
||||
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_STEP): cv.temperature_with_unit,
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
_WATER_HEATER_SCHEMA.add_extra(entity_duplicate_validator("water_heater"))
|
||||
_WATER_HEATER_SCHEMA.add_extra(_visual_temperatures_in_entity_unit)
|
||||
|
||||
|
||||
def water_heater_schema(
|
||||
@@ -61,8 +79,16 @@ def water_heater_schema(
|
||||
*,
|
||||
icon: str = cv.UNDEFINED,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
temperature_unit: bool = False,
|
||||
) -> cv.Schema:
|
||||
"""Build the schema of a water heater platform.
|
||||
|
||||
``temperature_unit`` adds ``unit_of_measurement`` for platforms whose temperatures are all
|
||||
in the unit the user picks; the class then needs ``set_temperature_unit(TemperatureUnit)``.
|
||||
"""
|
||||
schema = {cv.GenerateID(): cv.declare_id(class_)}
|
||||
if temperature_unit:
|
||||
schema[cv.Optional(CONF_UNIT_OF_MEASUREMENT)] = cv.temperature_unit
|
||||
|
||||
for key, default, validator in [
|
||||
(CONF_ICON, icon, cv.icon),
|
||||
|
||||
@@ -77,6 +77,9 @@ from esphome.const import (
|
||||
SCHEDULER_DONT_RUN,
|
||||
TYPE_GIT,
|
||||
TYPE_LOCAL,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_FAHRENHEIT,
|
||||
UNIT_KELVIN,
|
||||
Framework,
|
||||
Toolchain,
|
||||
__version__ as ESPHOME_VERSION,
|
||||
@@ -1416,7 +1419,10 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False):
|
||||
if match is None:
|
||||
raise Invalid(f"Expected {quantity} with unit, got {value}")
|
||||
|
||||
mantissa = float(match.group(1))
|
||||
try:
|
||||
mantissa = float(match.group(1))
|
||||
except ValueError as e:
|
||||
raise Invalid(f"Expected {quantity} with unit, got {value}") from e
|
||||
if match.group(2) not in METRIC_SUFFIXES:
|
||||
raise Invalid(f"Invalid {quantity} suffix {match.group(2)}")
|
||||
|
||||
@@ -1435,53 +1441,72 @@ distance = float_with_unit("distance", "(m)")
|
||||
framerate = float_with_unit("framerate", "(FPS|fps|Fps|FpS|Hz)")
|
||||
angle = float_with_unit("angle", "(°|deg)", optional_unit=True)
|
||||
_temperature_c = float_with_unit("temperature", "(°C|° C|C|°)?")
|
||||
_temperature_k = float_with_unit("temperature", "(°K|° K|K)?")
|
||||
_temperature_f = float_with_unit("temperature", "(°F|° F|F)?")
|
||||
_temperature_c_strict = float_with_unit("temperature", "(°C|° C|C)")
|
||||
_temperature_k = float_with_unit("temperature", "(°K|° K|K)")
|
||||
_temperature_f = float_with_unit("temperature", "(°F|° F|F)")
|
||||
decibel = float_with_unit("decibel", "(dB|dBm|db|dbm)", optional_unit=True)
|
||||
pressure = float_with_unit("pressure", "(bar|Bar)", optional_unit=True)
|
||||
|
||||
|
||||
def temperature(value):
|
||||
err = None
|
||||
try:
|
||||
return _temperature_c(value)
|
||||
except Invalid as orig_err:
|
||||
err = orig_err
|
||||
|
||||
try:
|
||||
kelvin = _temperature_k(value)
|
||||
return kelvin - 273.15
|
||||
except Invalid:
|
||||
pass
|
||||
|
||||
try:
|
||||
fahrenheit = _temperature_f(value)
|
||||
return (fahrenheit - 32) * (5 / 9)
|
||||
except Invalid:
|
||||
pass
|
||||
|
||||
raise err
|
||||
def temperature_unit(value: Any) -> str:
|
||||
"""Validate the unit an entity reports its temperatures in."""
|
||||
return one_of(UNIT_CELSIUS, UNIT_FAHRENHEIT, UNIT_KELVIN)(value)
|
||||
|
||||
|
||||
def temperature_delta(value):
|
||||
err = None
|
||||
try:
|
||||
return _temperature_c(value)
|
||||
except Invalid as orig_err:
|
||||
err = orig_err
|
||||
def temperature_with_unit(value: Any) -> tuple[float, str | None]:
|
||||
"""Parse a temperature and keep the unit written with it; the unit is None when there is none."""
|
||||
for validator, unit in (
|
||||
(_temperature_k, UNIT_KELVIN),
|
||||
(_temperature_f, UNIT_FAHRENHEIT),
|
||||
(_temperature_c_strict, UNIT_CELSIUS),
|
||||
):
|
||||
try:
|
||||
return validator(value), unit
|
||||
except Invalid:
|
||||
pass
|
||||
return _temperature_c(value), None
|
||||
|
||||
try:
|
||||
return _temperature_k(value)
|
||||
except Invalid:
|
||||
pass
|
||||
|
||||
try:
|
||||
fahrenheit = _temperature_f(value)
|
||||
return fahrenheit * (5 / 9)
|
||||
except Invalid:
|
||||
pass
|
||||
def _temperature_to_celsius(value: float, unit: str, delta: bool) -> float:
|
||||
if unit == UNIT_FAHRENHEIT:
|
||||
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
|
||||
|
||||
raise err
|
||||
|
||||
def _temperature_from_celsius(value: float, unit: str, delta: bool) -> float:
|
||||
if unit == UNIT_FAHRENHEIT:
|
||||
return value * (9 / 5) if delta else value * (9 / 5) + 32
|
||||
if unit == UNIT_KELVIN:
|
||||
return value if delta else value + 273.15
|
||||
return value
|
||||
|
||||
|
||||
def convert_temperature(
|
||||
value: float, unit: str | None, to_unit: str, *, delta: bool = False
|
||||
) -> float:
|
||||
"""Convert a temperature, or with ``delta`` a temperature difference, between units.
|
||||
|
||||
A value written without a unit is taken to be in ``to_unit`` already.
|
||||
"""
|
||||
if unit is None or unit == to_unit:
|
||||
return value
|
||||
return _temperature_from_celsius(
|
||||
_temperature_to_celsius(value, unit, delta), to_unit, delta
|
||||
)
|
||||
|
||||
|
||||
def temperature(value: Any) -> float:
|
||||
"""Parse a temperature in °C, K or °F into degrees Celsius."""
|
||||
return convert_temperature(*temperature_with_unit(value), UNIT_CELSIUS)
|
||||
|
||||
|
||||
def temperature_delta(value: Any) -> float:
|
||||
"""Parse a temperature difference in °C, K or °F into Celsius degrees."""
|
||||
return convert_temperature(*temperature_with_unit(value), UNIT_CELSIUS, delta=True)
|
||||
|
||||
|
||||
_color_temperature_mireds = float_with_unit("Color Temperature", r"(mireds|Mireds)")
|
||||
|
||||
@@ -1260,6 +1260,7 @@ UNIT_DECIBEL_MILLIWATT = "dBm"
|
||||
UNIT_DEGREE_PER_SECOND = "°/s"
|
||||
UNIT_DEGREES = "°"
|
||||
UNIT_EMPTY = ""
|
||||
UNIT_FAHRENHEIT = "°F"
|
||||
UNIT_G = "G"
|
||||
UNIT_GRAMS_PER_CUBIC_METER = "g/m³"
|
||||
UNIT_HECTOPASCAL = "hPa"
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from esphome.const import UNIT_CELSIUS, UNIT_FAHRENHEIT, UNIT_KELVIN
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
global_ns = MockObj("", "")
|
||||
@@ -42,6 +43,12 @@ InternalGPIOPin = esphome_ns.class_("InternalGPIOPin", GPIOPin)
|
||||
gpio_ns = esphome_ns.namespace("gpio")
|
||||
gpio_Flags = gpio_ns.enum("Flags", is_class=True)
|
||||
EntityCategory = esphome_ns.enum("EntityCategory")
|
||||
TemperatureUnit = esphome_ns.enum("TemperatureUnit", is_class=True)
|
||||
TEMPERATURE_UNITS = {
|
||||
UNIT_CELSIUS: TemperatureUnit.CELSIUS,
|
||||
UNIT_FAHRENHEIT: TemperatureUnit.FAHRENHEIT,
|
||||
UNIT_KELVIN: TemperatureUnit.KELVIN,
|
||||
}
|
||||
Parented = esphome_ns.class_("Parented")
|
||||
ESPTime = esphome_ns.struct("ESPTime")
|
||||
StringRef = esphome_ns.class_("StringRef")
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Tests for the climate visual temperature conversion."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.climate import (
|
||||
_visual_temperatures_in_entity_unit,
|
||||
visual_temperature_step,
|
||||
)
|
||||
from esphome.const import (
|
||||
CONF_CURRENT_TEMPERATURE,
|
||||
CONF_MAX_TEMPERATURE,
|
||||
CONF_MIN_TEMPERATURE,
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
CONF_TEMPERATURE_STEP,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VISUAL,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_FAHRENHEIT,
|
||||
)
|
||||
from esphome.core.entity_helpers import entity_temperature_unit
|
||||
|
||||
|
||||
def test_visual_temperature_step_scalar_applies_to_both() -> None:
|
||||
step = visual_temperature_step("1°F")
|
||||
assert step == {
|
||||
CONF_TARGET_TEMPERATURE: (1.0, UNIT_FAHRENHEIT),
|
||||
CONF_CURRENT_TEMPERATURE: (1.0, UNIT_FAHRENHEIT),
|
||||
}
|
||||
|
||||
|
||||
def test_visual_temperatures_default_to_celsius() -> None:
|
||||
config = _visual_temperatures_in_entity_unit(
|
||||
{
|
||||
CONF_VISUAL: {
|
||||
CONF_MIN_TEMPERATURE: (50.0, UNIT_FAHRENHEIT),
|
||||
CONF_MAX_TEMPERATURE: (30.0, None),
|
||||
CONF_TEMPERATURE_STEP: visual_temperature_step("1.8°F"),
|
||||
}
|
||||
}
|
||||
)
|
||||
visual = config[CONF_VISUAL]
|
||||
assert visual[CONF_MIN_TEMPERATURE] == pytest.approx(10.0)
|
||||
assert visual[CONF_MAX_TEMPERATURE] == 30.0
|
||||
assert visual[CONF_TEMPERATURE_STEP][CONF_TARGET_TEMPERATURE] == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_visual_temperatures_follow_unit_of_measurement() -> None:
|
||||
config = _visual_temperatures_in_entity_unit(
|
||||
{
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_FAHRENHEIT,
|
||||
CONF_VISUAL: {
|
||||
CONF_MIN_TEMPERATURE: (60.0, None),
|
||||
CONF_MAX_TEMPERATURE: (30.0, UNIT_CELSIUS),
|
||||
CONF_TEMPERATURE_STEP: {
|
||||
CONF_TARGET_TEMPERATURE: (1.0, UNIT_FAHRENHEIT),
|
||||
CONF_CURRENT_TEMPERATURE: (0.5, UNIT_CELSIUS),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
visual = config[CONF_VISUAL]
|
||||
assert visual[CONF_MIN_TEMPERATURE] == 60.0
|
||||
assert visual[CONF_MAX_TEMPERATURE] == pytest.approx(86.0)
|
||||
assert visual[CONF_TEMPERATURE_STEP][CONF_TARGET_TEMPERATURE] == 1.0
|
||||
assert visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE] == pytest.approx(0.9)
|
||||
|
||||
|
||||
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 entity_temperature_unit({CONF_UNIT_OF_MEASUREMENT: "f"}) == UNIT_CELSIUS
|
||||
assert (
|
||||
entity_temperature_unit({CONF_UNIT_OF_MEASUREMENT: UNIT_FAHRENHEIT})
|
||||
== UNIT_FAHRENHEIT
|
||||
)
|
||||
|
||||
|
||||
def test_visual_temperatures_converted_once() -> None:
|
||||
config = {CONF_VISUAL: {CONF_MIN_TEMPERATURE: (50.0, UNIT_FAHRENHEIT)}}
|
||||
_visual_temperatures_in_entity_unit(config)
|
||||
_visual_temperatures_in_entity_unit(config)
|
||||
assert config[CONF_VISUAL][CONF_MIN_TEMPERATURE] == pytest.approx(10.0)
|
||||
@@ -24,6 +24,8 @@ from esphome.const import (
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
CONF_TARGET_TEMPERATURE_HIGH,
|
||||
CONF_TARGET_TEMPERATURE_LOW,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
UNIT_FAHRENHEIT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -143,3 +145,14 @@ def test_initial_state_matching_two_point_accepted() -> None:
|
||||
},
|
||||
}
|
||||
assert _validate_initial_state(config) is config
|
||||
|
||||
|
||||
def test_initial_state_temperature_follows_unit_of_measurement() -> None:
|
||||
config: ConfigType = {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_FAHRENHEIT,
|
||||
CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: False,
|
||||
CONF_SUPPORTS_TARGET_HUMIDITY: False,
|
||||
CONF_INITIAL_STATE: {CONF_TARGET_TEMPERATURE: (21.0, "°C")},
|
||||
}
|
||||
_validate_initial_state(config)
|
||||
assert config[CONF_INITIAL_STATE][CONF_TARGET_TEMPERATURE] == pytest.approx(69.8)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Tests for the water heater visual temperature conversion."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.water_heater import (
|
||||
CONF_TARGET_TEMPERATURE_STEP,
|
||||
_visual_temperatures_in_entity_unit,
|
||||
)
|
||||
from esphome.const import (
|
||||
CONF_MAX_TEMPERATURE,
|
||||
CONF_MIN_TEMPERATURE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VISUAL,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_FAHRENHEIT,
|
||||
)
|
||||
|
||||
|
||||
def test_visual_temperatures_default_to_celsius() -> None:
|
||||
config = _visual_temperatures_in_entity_unit(
|
||||
{
|
||||
CONF_VISUAL: {
|
||||
CONF_MIN_TEMPERATURE: (100.0, UNIT_FAHRENHEIT),
|
||||
CONF_MAX_TEMPERATURE: (85.0, None),
|
||||
CONF_TARGET_TEMPERATURE_STEP: (1.8, UNIT_FAHRENHEIT),
|
||||
}
|
||||
}
|
||||
)
|
||||
visual = config[CONF_VISUAL]
|
||||
assert visual[CONF_MIN_TEMPERATURE] == pytest.approx(37.78, abs=0.01)
|
||||
assert visual[CONF_MAX_TEMPERATURE] == 85.0
|
||||
assert visual[CONF_TARGET_TEMPERATURE_STEP] == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_visual_temperatures_follow_unit_of_measurement() -> None:
|
||||
config = _visual_temperatures_in_entity_unit(
|
||||
{
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_FAHRENHEIT,
|
||||
CONF_VISUAL: {
|
||||
CONF_MIN_TEMPERATURE: (100.0, None),
|
||||
CONF_MAX_TEMPERATURE: (60.0, UNIT_CELSIUS),
|
||||
CONF_TARGET_TEMPERATURE_STEP: (1.0, UNIT_CELSIUS),
|
||||
},
|
||||
}
|
||||
)
|
||||
visual = config[CONF_VISUAL]
|
||||
assert visual[CONF_MIN_TEMPERATURE] == 100.0
|
||||
assert visual[CONF_MAX_TEMPERATURE] == pytest.approx(140.0)
|
||||
assert visual[CONF_TARGET_TEMPERATURE_STEP] == pytest.approx(1.8)
|
||||
@@ -41,6 +41,17 @@ esphome:
|
||||
mode: !lambda "return climate::CLIMATE_MODE_COOL;"
|
||||
target_temperature: !lambda "return 23.0f;"
|
||||
|
||||
- climate.template.publish:
|
||||
id: template_climate_fahrenheit
|
||||
current_temperature: 20°C
|
||||
target_temperature: 70
|
||||
- climate.control:
|
||||
id: template_climate_fahrenheit
|
||||
target_temperature: 22°C
|
||||
- water_heater.template.publish:
|
||||
id: template_water_heater_fahrenheit
|
||||
current_temperature: 45°C
|
||||
target_temperature: 120
|
||||
- climate.template.publish:
|
||||
id: template_climate_custom_modes
|
||||
custom_fan_mode: "turbo"
|
||||
@@ -592,6 +603,25 @@ alarm_control_panel:
|
||||
- "1234"
|
||||
|
||||
climate:
|
||||
# unit_of_measurement keeps the visual temperatures in that unit; a value written in
|
||||
# another unit is converted into it
|
||||
- platform: template
|
||||
id: template_climate_fahrenheit
|
||||
name: "Template Climate Fahrenheit"
|
||||
unit_of_measurement: °F
|
||||
optimistic: true
|
||||
supported_modes:
|
||||
- "OFF"
|
||||
- HEAT
|
||||
initial_state:
|
||||
mode: HEAT
|
||||
target_temperature: 21°C
|
||||
visual:
|
||||
min_temperature: 60
|
||||
max_temperature: 30°C
|
||||
temperature_step:
|
||||
target_temperature: 1°F
|
||||
current_temperature: 0.5°C
|
||||
- platform: template
|
||||
id: template_climate
|
||||
name: "Template Climate"
|
||||
@@ -684,6 +714,17 @@ climate:
|
||||
temperature_step: 0.5
|
||||
|
||||
water_heater:
|
||||
- platform: template
|
||||
id: template_water_heater_fahrenheit
|
||||
name: "Template Water Heater Fahrenheit"
|
||||
unit_of_measurement: °F
|
||||
optimistic: true
|
||||
current_temperature: !lambda "return 110.0f;"
|
||||
target_temperature: !lambda "return 120.0f;"
|
||||
visual:
|
||||
min_temperature: 100
|
||||
max_temperature: 60°C
|
||||
target_temperature_step: 1°C
|
||||
- platform: template
|
||||
id: template_water_heater
|
||||
name: "Template Water Heater"
|
||||
|
||||
@@ -2,15 +2,11 @@ water_heater:
|
||||
- platform: template
|
||||
id: my_boiler
|
||||
name: "Test Boiler"
|
||||
min_temperature: 10
|
||||
max_temperature: 85
|
||||
target_temperature_step: 0.5
|
||||
current_temperature_step: 0.1
|
||||
optimistic: true
|
||||
current_temperature: 45.0
|
||||
current_temperature: !lambda "return 45.0f;"
|
||||
target_temperature: !lambda "return 60.0f;"
|
||||
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
|
||||
visual:
|
||||
min_temperature: 10
|
||||
max_temperature: 85
|
||||
target_temperature_step: 0.5
|
||||
current_temperature_step: 0.1
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
water_heater: !include common.yaml
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
water_heater: !include common.yaml
|
||||
@@ -51,6 +51,9 @@ from esphome.const import (
|
||||
SCHEDULER_DONT_RUN,
|
||||
TYPE_GIT,
|
||||
TYPE_LOCAL,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_FAHRENHEIT,
|
||||
UNIT_KELVIN,
|
||||
Framework,
|
||||
Toolchain,
|
||||
)
|
||||
@@ -2145,6 +2148,60 @@ def test_temperature_delta_invalid() -> None:
|
||||
cv.temperature_delta("5x")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("100°F", (100.0, UNIT_FAHRENHEIT)),
|
||||
("100 °F", (100.0, UNIT_FAHRENHEIT)),
|
||||
("100F", (100.0, UNIT_FAHRENHEIT)),
|
||||
("25°C", (25.0, UNIT_CELSIUS)),
|
||||
("25C", (25.0, UNIT_CELSIUS)),
|
||||
("300K", (300.0, UNIT_KELVIN)),
|
||||
("300°K", (300.0, UNIT_KELVIN)),
|
||||
("25", (25.0, None)),
|
||||
("25°", (25.0, None)),
|
||||
(25, (25.0, None)),
|
||||
],
|
||||
)
|
||||
def test_temperature_with_unit(
|
||||
value: str | int, expected: tuple[float, str | None]
|
||||
) -> None:
|
||||
assert cv.temperature_with_unit(value) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["5x", "abc°F", None])
|
||||
def test_temperature_with_unit__invalid(value: str | None) -> None:
|
||||
with pytest.raises(Invalid):
|
||||
cv.temperature_with_unit(value)
|
||||
|
||||
|
||||
def test_temperature_unit() -> None:
|
||||
assert cv.temperature_unit("°F") == UNIT_FAHRENHEIT
|
||||
with pytest.raises(Invalid):
|
||||
cv.temperature_unit("F")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "unit", "to_unit", "delta", "expected"),
|
||||
[
|
||||
(60.0, None, UNIT_FAHRENHEIT, False, 60.0),
|
||||
(30.0, UNIT_CELSIUS, UNIT_FAHRENHEIT, False, 86.0),
|
||||
(212.0, UNIT_FAHRENHEIT, UNIT_CELSIUS, False, 100.0),
|
||||
(300.0, UNIT_KELVIN, UNIT_FAHRENHEIT, False, 80.33),
|
||||
(86.0, UNIT_FAHRENHEIT, UNIT_KELVIN, False, 303.15),
|
||||
(0.5, UNIT_CELSIUS, UNIT_FAHRENHEIT, True, 0.9),
|
||||
(9.0, UNIT_FAHRENHEIT, UNIT_CELSIUS, True, 5.0),
|
||||
(1.0, UNIT_KELVIN, UNIT_CELSIUS, True, 1.0),
|
||||
],
|
||||
)
|
||||
def test_convert_temperature(
|
||||
value: float, unit: str | None, to_unit: str, delta: bool, expected: float
|
||||
) -> None:
|
||||
assert cv.convert_temperature(value, unit, to_unit, delta=delta) == pytest.approx(
|
||||
expected
|
||||
)
|
||||
|
||||
|
||||
def test_color_temperature_mireds() -> None:
|
||||
assert cv.color_temperature("153 mireds") == pytest.approx(153.0)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user