Skip to content

Commit 955e836

Browse files
authored
Add template number device_class (#168438)
1 parent 1fc0b62 commit 955e836

5 files changed

Lines changed: 182 additions & 25 deletions

File tree

homeassistant/components/template/config_flow.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from homeassistant.components.button import ButtonDeviceClass
1414
from homeassistant.components.cover import CoverDeviceClass
1515
from homeassistant.components.event import EventDeviceClass
16+
from homeassistant.components.number import NumberDeviceClass
1617
from homeassistant.components.sensor import (
1718
CONF_STATE_CLASS,
1819
DEVICE_CLASS_STATE_CLASSES,
@@ -286,6 +287,14 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
286287

287288
if domain == Platform.NUMBER:
288289
schema |= {
290+
vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector(
291+
selector.SelectSelectorConfig(
292+
options=[cls.value for cls in NumberDeviceClass],
293+
mode=selector.SelectSelectorMode.DROPDOWN,
294+
translation_key="number_device_class",
295+
sort=True,
296+
),
297+
),
289298
vol.Required(CONF_STATE): selector.TemplateSelector(),
290299
vol.Required(CONF_MIN, default=DEFAULT_MIN_VALUE): selector.NumberSelector(
291300
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),

homeassistant/components/template/number.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@
1111
DEFAULT_MAX_VALUE,
1212
DEFAULT_MIN_VALUE,
1313
DEFAULT_STEP,
14+
DEVICE_CLASSES_SCHEMA,
1415
DOMAIN as NUMBER_DOMAIN,
1516
ENTITY_ID_FORMAT,
1617
NumberEntity,
1718
)
1819
from homeassistant.config_entries import ConfigEntry
19-
from homeassistant.const import CONF_NAME, CONF_STATE, CONF_UNIT_OF_MEASUREMENT
20+
from homeassistant.const import (
21+
CONF_DEVICE_CLASS,
22+
CONF_NAME,
23+
CONF_STATE,
24+
CONF_UNIT_OF_MEASUREMENT,
25+
)
2026
from homeassistant.core import HomeAssistant, callback
2127
from homeassistant.helpers import config_validation as cv
2228
from homeassistant.helpers.entity_platform import (
@@ -50,6 +56,7 @@
5056

5157
NUMBER_COMMON_SCHEMA = vol.Schema(
5258
{
59+
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
5360
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
5461
vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): cv.template,
5562
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
@@ -124,6 +131,7 @@ class AbstractTemplateNumber(AbstractTemplateEntity, NumberEntity):
124131
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
125132
def __init__(self, name: str, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called
126133
"""Initialize the features."""
134+
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
127135
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
128136
self._attr_native_step = DEFAULT_STEP
129137
self._attr_native_min_value = DEFAULT_MIN_VALUE

homeassistant/components/template/strings.json

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@
292292
},
293293
"number": {
294294
"data": {
295+
"device_class": "[%key:component::template::common::device_class%]",
295296
"device_id": "[%key:common::config_flow::data::device%]",
296297
"max": "Maximum value",
297298
"min": "Minimum value",
@@ -836,6 +837,7 @@
836837
},
837838
"number": {
838839
"data": {
840+
"device_class": "[%key:component::template::common::device_class%]",
839841
"device_id": "[%key:common::config_flow::data::device%]",
840842
"max": "[%key:component::template::config::step::number::data::max%]",
841843
"min": "[%key:component::template::config::step::number::data::min%]",
@@ -1128,6 +1130,62 @@
11281130
"motion": "[%key:component::event::entity_component::motion::name%]"
11291131
}
11301132
},
1133+
"number_device_class": {
1134+
"options": {
1135+
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
1136+
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
1137+
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
1138+
"area": "[%key:component::sensor::entity_component::area::name%]",
1139+
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
1140+
"battery": "[%key:component::sensor::entity_component::battery::name%]",
1141+
"blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]",
1142+
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
1143+
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
1144+
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
1145+
"current": "[%key:component::sensor::entity_component::current::name%]",
1146+
"data_rate": "[%key:component::sensor::entity_component::data_rate::name%]",
1147+
"distance": "[%key:component::sensor::entity_component::distance::name%]",
1148+
"energy": "[%key:component::sensor::entity_component::energy::name%]",
1149+
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
1150+
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
1151+
"gas": "[%key:component::sensor::entity_component::gas::name%]",
1152+
"humidity": "[%key:component::sensor::entity_component::humidity::name%]",
1153+
"illuminance": "[%key:component::sensor::entity_component::illuminance::name%]",
1154+
"irradiance": "[%key:component::sensor::entity_component::irradiance::name%]",
1155+
"moisture": "[%key:component::sensor::entity_component::moisture::name%]",
1156+
"nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
1157+
"nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
1158+
"nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]",
1159+
"ozone": "[%key:component::sensor::entity_component::ozone::name%]",
1160+
"ph": "[%key:component::sensor::entity_component::ph::name%]",
1161+
"pm1": "[%key:component::sensor::entity_component::pm1::name%]",
1162+
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
1163+
"pm25": "[%key:component::sensor::entity_component::pm25::name%]",
1164+
"pm4": "[%key:component::sensor::entity_component::pm4::name%]",
1165+
"power": "[%key:component::sensor::entity_component::power::name%]",
1166+
"power_factor": "[%key:component::sensor::entity_component::power_factor::name%]",
1167+
"precipitation": "[%key:component::sensor::entity_component::precipitation::name%]",
1168+
"precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]",
1169+
"pressure": "[%key:component::sensor::entity_component::pressure::name%]",
1170+
"reactive_energy": "[%key:component::sensor::entity_component::reactive_energy::name%]",
1171+
"reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]",
1172+
"signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]",
1173+
"sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]",
1174+
"speed": "[%key:component::sensor::entity_component::speed::name%]",
1175+
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
1176+
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
1177+
"temperature_delta": "[%key:component::sensor::entity_component::temperature_delta::name%]",
1178+
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
1179+
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
1180+
"volume": "[%key:component::sensor::entity_component::volume::name%]",
1181+
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
1182+
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
1183+
"water": "[%key:component::sensor::entity_component::water::name%]",
1184+
"weight": "[%key:component::sensor::entity_component::weight::name%]",
1185+
"wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]",
1186+
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
1187+
}
1188+
},
11311189
"sensor_device_class": {
11321190
"options": {
11331191
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
@@ -1142,20 +1200,15 @@
11421200
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
11431201
"current": "[%key:component::sensor::entity_component::current::name%]",
11441202
"data_rate": "[%key:component::sensor::entity_component::data_rate::name%]",
1145-
"data_size": "[%key:component::sensor::entity_component::data_size::name%]",
1146-
"date": "[%key:component::sensor::entity_component::date::name%]",
11471203
"distance": "[%key:component::sensor::entity_component::distance::name%]",
1148-
"duration": "[%key:component::sensor::entity_component::duration::name%]",
11491204
"energy": "[%key:component::sensor::entity_component::energy::name%]",
1150-
"energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]",
11511205
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
11521206
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
11531207
"gas": "[%key:component::sensor::entity_component::gas::name%]",
11541208
"humidity": "[%key:component::sensor::entity_component::humidity::name%]",
11551209
"illuminance": "[%key:component::sensor::entity_component::illuminance::name%]",
11561210
"irradiance": "[%key:component::sensor::entity_component::irradiance::name%]",
11571211
"moisture": "[%key:component::sensor::entity_component::moisture::name%]",
1158-
"monetary": "[%key:component::sensor::entity_component::monetary::name%]",
11591212
"nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
11601213
"nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
11611214
"nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]",
@@ -1178,7 +1231,6 @@
11781231
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
11791232
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
11801233
"temperature_delta": "[%key:component::sensor::entity_component::temperature_delta::name%]",
1181-
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
11821234
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
11831235
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]",
11841236
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",

tests/components/template/test_config_flow.py

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,7 @@ async def test_config_flow_device(
708708
"min": 0,
709709
"max": 100,
710710
"step": 0.1,
711+
"device_class": "distance",
711712
"unit_of_measurement": "cm",
712713
"set_value": {
713714
"action": "input_number.set_value",
@@ -719,15 +720,16 @@ async def test_config_flow_device(
719720
"min": 0,
720721
"max": 100,
721722
"step": 0.1,
722-
"unit_of_measurement": "cm",
723+
"device_class": "current",
724+
"unit_of_measurement": "mA",
723725
"set_value": {
724726
"action": "input_number.set_value",
725727
"target": {"entity_id": "input_number.test"},
726728
"data": {"value": "{{ value }}"},
727729
},
728730
},
729731
"state",
730-
None,
732+
"distance",
731733
),
732734
(
733735
"alarm_control_panel",
@@ -901,27 +903,76 @@ async def test_options(
901903
)
902904

903905

906+
@pytest.mark.parametrize(
907+
(
908+
"template_type",
909+
"old_state_template",
910+
"new_state_template",
911+
"input_states",
912+
"extra_options",
913+
"suggested_device_class",
914+
),
915+
[
916+
(
917+
"binary_sensor",
918+
{
919+
"state": "{{ states('binary_sensor.one') == 'on' or states('binary_sensor.two') == 'on' }}"
920+
},
921+
{
922+
"state": "{{ states('binary_sensor.one') == 'on' and states('binary_sensor.two') == 'on' }}"
923+
},
924+
{"one": "on", "two": "off"},
925+
{"device_class": "motion"},
926+
"motion",
927+
),
928+
(
929+
"number",
930+
{"state": "{{ states('number.one') }}"},
931+
{"state": "{{ states('number.two') }}"},
932+
{"one": "30.0", "two": "20.0"},
933+
{
934+
"min": 0,
935+
"max": 100,
936+
"step": 0.1,
937+
"device_class": "distance",
938+
"unit_of_measurement": "cm",
939+
"set_value": {
940+
"action": "input_number.set_value",
941+
"target": {"entity_id": "input_number.test"},
942+
"data": {"value": "{{ value }}"},
943+
},
944+
},
945+
"distance",
946+
),
947+
],
948+
)
904949
@pytest.mark.freeze_time("2024-07-09 00:00:00+00:00")
905-
async def test_options_binary_sensor_remove_device_class(hass: HomeAssistant) -> None:
906-
"""Test removing the binary sensor device class in options."""
907-
hass.states.async_set("binary_sensor.one", "on", {})
908-
hass.states.async_set("binary_sensor.two", "off", {})
950+
async def test_options_remove_device_class(
951+
hass: HomeAssistant,
952+
template_type: str,
953+
old_state_template: dict[str, Any],
954+
new_state_template: dict[str, Any],
955+
input_states: dict[str, Any],
956+
extra_options: dict[str, Any],
957+
suggested_device_class: str | None,
958+
) -> None:
959+
"""Test removing the device class in options."""
909960

910-
old_state_template = {
911-
"state": "{{ states('binary_sensor.one') == 'on' or states('binary_sensor.two') == 'on' }}"
912-
}
913-
new_state_template = {
914-
"state": "{{ states('binary_sensor.one') == 'on' and states('binary_sensor.two') == 'on' }}"
915-
}
961+
input_entities = ["one", "two"]
962+
963+
for input_entity in input_entities:
964+
hass.states.async_set(
965+
f"{template_type}.{input_entity}", input_states[input_entity], {}
966+
)
916967

917968
config_entry = MockConfigEntry(
918969
data={},
919970
domain=DOMAIN,
920971
options={
921972
"name": "My template",
922-
"template_type": "binary_sensor",
973+
"template_type": template_type,
923974
**old_state_template,
924-
"device_class": "motion",
975+
**extra_options,
925976
},
926977
title="My template",
927978
)
@@ -932,28 +983,32 @@ async def test_options_binary_sensor_remove_device_class(hass: HomeAssistant) ->
932983

933984
result = await hass.config_entries.options.async_init(config_entry.entry_id)
934985
assert result["type"] is FlowResultType.FORM
935-
assert result["step_id"] == "binary_sensor"
986+
assert result["step_id"] == template_type
936987
assert (
937988
get_schema_suggested_value(result["data_schema"].schema, "device_class")
938-
== "motion"
989+
== suggested_device_class
939990
)
940991

992+
extra_options.pop("device_class")
941993
result = await hass.config_entries.options.async_configure(
942994
result["flow_id"],
943995
user_input={
944996
**new_state_template,
997+
**extra_options,
945998
},
946999
)
9471000
assert result["type"] is FlowResultType.CREATE_ENTRY
9481001
assert result["data"] == {
9491002
"name": "My template",
950-
"template_type": "binary_sensor",
1003+
"template_type": template_type,
9511004
**new_state_template,
1005+
**extra_options,
9521006
}
9531007
assert config_entry.options == {
9541008
"name": "My template",
955-
"template_type": "binary_sensor",
1009+
"template_type": template_type,
9561010
**new_state_template,
1011+
**extra_options,
9571012
}
9581013
assert "device_class" not in config_entry.options
9591014

tests/components/template/test_number.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,3 +564,36 @@ async def test_nested_unique_id(
564564
await setup_and_test_nested_unique_id(
565565
hass, TEST_NUMBER, style, entity_registry, TEST_REQUIRED, "{{ 0 }}"
566566
)
567+
568+
569+
@pytest.mark.parametrize("count", [1])
570+
@pytest.mark.parametrize(
571+
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
572+
)
573+
@pytest.mark.parametrize(
574+
("config", "expected_device_class"),
575+
[
576+
(
577+
{
578+
**TEST_REQUIRED,
579+
"unit_of_measurement": "°C",
580+
"device_class": "temperature",
581+
},
582+
"temperature",
583+
),
584+
(
585+
TEST_REQUIRED,
586+
None,
587+
),
588+
],
589+
)
590+
@pytest.mark.usefixtures("setup_number")
591+
async def test_setup_valid_device_class(
592+
hass: HomeAssistant, expected_device_class: str | None
593+
) -> None:
594+
"""Test setup with valid device_class."""
595+
await async_trigger(hass, TEST_STATE_ENTITY_ID, "75")
596+
assert (
597+
hass.states.get(TEST_NUMBER.entity_id).attributes.get("device_class")
598+
== expected_device_class
599+
)

0 commit comments

Comments
 (0)