mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 23:41:48 -05:00
Add state class selector (#180667)
This commit is contained in:
@@ -26,6 +26,8 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
StateClassSelector,
|
||||
StateClassSelectorConfig,
|
||||
StateSelector,
|
||||
StateSelectorConfig,
|
||||
TemplateSelector,
|
||||
@@ -142,12 +144,8 @@ def _get_options_schema_with_entity_id(entity_id: str, type: str) -> vol.Schema:
|
||||
vol.Optional(CONF_DURATION): DurationSelector(
|
||||
DurationSelectorConfig(enable_day=True, allow_negative=False),
|
||||
),
|
||||
vol.Optional(CONF_STATE_CLASS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=state_class_options,
|
||||
translation_key=CONF_STATE_CLASS,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
vol.Optional(CONF_STATE_CLASS): StateClassSelector(
|
||||
StateClassSelectorConfig(state_classes=state_class_options),
|
||||
),
|
||||
vol.Optional(SECTION_ADDITIONAL_SETTINGS): section(
|
||||
vol.Schema(
|
||||
|
||||
@@ -107,12 +107,6 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"state_class": {
|
||||
"options": {
|
||||
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
|
||||
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"options": {
|
||||
"count": "Count",
|
||||
|
||||
@@ -16,7 +16,6 @@ from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS as CONF_SENSOR_STATE_CLASS,
|
||||
DEVICE_CLASS_UNITS as SENSOR_DEVICE_CLASS_UNITS,
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.text import TextMode
|
||||
from homeassistant.const import (
|
||||
@@ -955,13 +954,7 @@ SENSOR_KNX_SCHEMA = AllSerializeFirst(
|
||||
sort=True,
|
||||
)
|
||||
),
|
||||
probatio.Optional(CONF_SENSOR_STATE_CLASS): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=list(SensorStateClass),
|
||||
translation_key="component.knx.selector.sensor_state_class",
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
probatio.Optional(CONF_SENSOR_STATE_CLASS): selector.StateClassSelector(),
|
||||
probatio.Optional(CONF_ALWAYS_CALLBACK): selector.BooleanSelector(),
|
||||
probatio.Required(CONF_SYNC_STATE, default=True): SyncStateSelector(
|
||||
allow_false=True
|
||||
|
||||
@@ -1389,14 +1389,6 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"sensor_state_class": {
|
||||
"options": {
|
||||
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
|
||||
"measurement_angle": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement_angle%]",
|
||||
"total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]",
|
||||
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
|
||||
}
|
||||
},
|
||||
"telegram_backend": {
|
||||
"options": {
|
||||
"postgres": "PostgreSQL (External)",
|
||||
|
||||
@@ -52,7 +52,6 @@ from homeassistant.components.sensor import (
|
||||
DEVICE_CLASS_UNITS,
|
||||
STATE_CLASS_UNITS,
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.valve import ValveState
|
||||
from homeassistant.config_entries import (
|
||||
@@ -122,6 +121,7 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
StateClassSelector,
|
||||
TemplateSelector,
|
||||
TemplateSelectorConfig,
|
||||
TextSelector,
|
||||
@@ -761,13 +761,7 @@ SENSOR_ENTITY_CATEGORY_SELECTOR = SelectSelector(
|
||||
sort=True,
|
||||
)
|
||||
)
|
||||
SENSOR_STATE_CLASS_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[device_class.value for device_class in SensorStateClass],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key=CONF_STATE_CLASS,
|
||||
)
|
||||
)
|
||||
SENSOR_STATE_CLASS_SELECTOR = StateClassSelector()
|
||||
STEP_SELECTOR = NumberSelector(NumberSelectorConfig(min=1e-3, step=1e-3))
|
||||
SUPPORTED_COLOR_MODES_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
|
||||
@@ -1320,14 +1320,6 @@
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"state_class": {
|
||||
"options": {
|
||||
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
|
||||
"measurement_angle": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement_angle%]",
|
||||
"total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]",
|
||||
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
|
||||
}
|
||||
},
|
||||
"supported_color_modes": {
|
||||
"options": {
|
||||
"brightness": "[%key:component::light::entity_component::_::state_attributes::color_mode::state::brightness%]",
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.components.rest.schema import ( # pylint: disable=home-assis
|
||||
DEFAULT_METHOD,
|
||||
METHODS,
|
||||
)
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS, SensorStateClass
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_USER,
|
||||
ConfigEntry,
|
||||
@@ -59,6 +59,7 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
StateClassSelector,
|
||||
TemplateSelector,
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
@@ -155,14 +156,7 @@ SENSOR_SETTINGS = vol.Schema(
|
||||
vol.Optional(CONF_DEVICE_CLASS): DeviceClassSelector(
|
||||
DeviceClassSelectorConfig(domain=Platform.SENSOR)
|
||||
),
|
||||
vol.Optional(CONF_STATE_CLASS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[cls.value for cls in SensorStateClass],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key="state_class",
|
||||
sort=True,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_STATE_CLASS): StateClassSelector(),
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[cls.value for cls in UnitOfTemperature],
|
||||
|
||||
@@ -177,14 +177,6 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"state_class": {
|
||||
"options": {
|
||||
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
|
||||
"measurement_angle": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement_angle%]",
|
||||
"total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]",
|
||||
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
|
||||
}
|
||||
},
|
||||
"unit_of_measurement": {
|
||||
"options": {
|
||||
"none": "No unit of measurement"
|
||||
|
||||
@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session, scoped_session, sessionmaker
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.recorder import CONF_DB_URL, get_instance
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS, SensorStateClass
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
@@ -55,14 +55,7 @@ OPTIONS_SCHEMA: vol.Schema = vol.Schema(
|
||||
vol.Optional(CONF_DEVICE_CLASS): selector.DeviceClassSelector(
|
||||
selector.DeviceClassSelectorConfig(domain=Platform.SENSOR)
|
||||
),
|
||||
vol.Optional(CONF_STATE_CLASS): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[cls.value for cls in SensorStateClass],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
translation_key="state_class",
|
||||
sort=True,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_STATE_CLASS): selector.StateClassSelector(),
|
||||
}
|
||||
),
|
||||
{"collapsed": True},
|
||||
|
||||
@@ -107,16 +107,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"state_class": {
|
||||
"options": {
|
||||
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
|
||||
"measurement_angle": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement_angle%]",
|
||||
"total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]",
|
||||
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"query": {
|
||||
"description": "Executes a SQL query and returns the result.",
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
DEVICE_CLASS_STATE_CLASSES,
|
||||
DEVICE_CLASS_UNITS,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
@@ -324,14 +323,7 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
|
||||
vol.Optional(CONF_DEVICE_CLASS): selector.DeviceClassSelector(
|
||||
selector.DeviceClassSelectorConfig(domain=Platform.SENSOR),
|
||||
),
|
||||
vol.Optional(CONF_STATE_CLASS): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[cls.value for cls in SensorStateClass],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
translation_key="sensor_state_class",
|
||||
sort=True,
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_STATE_CLASS): selector.StateClassSelector(),
|
||||
}
|
||||
|
||||
if domain == Platform.SWITCH:
|
||||
|
||||
@@ -1146,14 +1146,6 @@
|
||||
"text": "Text"
|
||||
}
|
||||
},
|
||||
"sensor_state_class": {
|
||||
"options": {
|
||||
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
|
||||
"measurement_angle": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement_angle%]",
|
||||
"total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]",
|
||||
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
|
||||
}
|
||||
},
|
||||
"sensor_unit_of_measurement": {
|
||||
"options": {
|
||||
"none": "No unit of measurement"
|
||||
|
||||
@@ -5,7 +5,7 @@ from copy import deepcopy
|
||||
from enum import StrEnum
|
||||
from functools import cache
|
||||
import importlib
|
||||
from typing import Any, Literal, Required, TypedDict, cast, override
|
||||
from typing import TYPE_CHECKING, Any, Literal, Required, TypedDict, cast, override
|
||||
from uuid import UUID
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -20,6 +20,9 @@ from . import config_validation as cv
|
||||
|
||||
SELECTORS: decorator.Registry[str, type[Selector]] = decorator.Registry()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.components.sensor import SensorStateClass
|
||||
|
||||
|
||||
def _get_selector_type_and_class(config: Any) -> tuple[str, type[Selector]]:
|
||||
"""Get selector type and class."""
|
||||
@@ -1966,6 +1969,57 @@ class SerialPortSelector(Selector[SerialPortSelectorConfig]):
|
||||
return serial
|
||||
|
||||
|
||||
class StateClassSelectorConfig(BaseSelectorConfig, total=False):
|
||||
"""Class to represent a sensor state class selector config."""
|
||||
|
||||
multiple: bool
|
||||
state_classes: Sequence[str | SensorStateClass]
|
||||
|
||||
|
||||
@SELECTORS.register("state_class")
|
||||
class StateClassSelector(Selector[StateClassSelectorConfig]):
|
||||
"""Selector for sensor state class."""
|
||||
|
||||
selector_type = "state_class"
|
||||
|
||||
@staticmethod
|
||||
def _valid_state_classes(options: list[str]) -> list[str]:
|
||||
"""Validate state classes and raise if invalid."""
|
||||
vol.In(_enum_options(Platform.SENSOR, "SensorStateClass"))(options)
|
||||
return options
|
||||
|
||||
CONFIG_SCHEMA = vol.All(
|
||||
make_selector_config_schema(
|
||||
{
|
||||
vol.Optional("multiple", default=False): cv.boolean,
|
||||
vol.Optional("state_classes"): vol.All(
|
||||
cv.ensure_list, [str], [_valid_state_classes]
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self, config: StateClassSelectorConfig | None = None) -> None:
|
||||
"""Instantiate a state class selector."""
|
||||
super().__init__(config)
|
||||
|
||||
def __call__(self, data: Any) -> Any:
|
||||
"""Validate the passed selection."""
|
||||
state_classes_filter = self.config.get("state_classes")
|
||||
valid_options = [
|
||||
option
|
||||
for option in _enum_options(Platform.SENSOR, "SensorStateClass")
|
||||
if state_classes_filter is None or option in state_classes_filter
|
||||
]
|
||||
options_schema = vol.In(valid_options)
|
||||
|
||||
if not self.config["multiple"]:
|
||||
return options_schema(vol.Schema(str)(data))
|
||||
if not isinstance(data, list):
|
||||
raise vol.Invalid("Value should be a list")
|
||||
return [options_schema(vol.Schema(str)(val)) for val in data]
|
||||
|
||||
|
||||
class StateSelectorConfig(BaseSelectorConfig, total=False):
|
||||
"""Class to represent a state selector config."""
|
||||
|
||||
|
||||
@@ -2420,18 +2420,8 @@
|
||||
'optional': True,
|
||||
'required': False,
|
||||
'selector': dict({
|
||||
'select': dict({
|
||||
'custom_value': False,
|
||||
'mode': 'dropdown',
|
||||
'state_class': dict({
|
||||
'multiple': False,
|
||||
'options': list([
|
||||
'measurement',
|
||||
'measurement_angle',
|
||||
'total',
|
||||
'total_increasing',
|
||||
]),
|
||||
'sort': False,
|
||||
'translation_key': 'component.knx.selector.sensor_state_class',
|
||||
}),
|
||||
}),
|
||||
'type': 'ha_selector',
|
||||
|
||||
@@ -1699,6 +1699,73 @@ def test_device_class_selector_schema(
|
||||
_test_selector("device_class", schema, valid_selections, invalid_selections)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("schema", "raises"),
|
||||
[
|
||||
(None, does_not_raise()),
|
||||
({}, does_not_raise()),
|
||||
({"multiple": False}, does_not_raise()),
|
||||
({"multiple": True}, does_not_raise()),
|
||||
({"state_classes": "total"}, does_not_raise()),
|
||||
({"state_classes": ["total"]}, does_not_raise()),
|
||||
({"state_classes": ["total", "measurement"]}, does_not_raise()),
|
||||
({"state_classes": ["cat"]}, pytest.raises(vol.Invalid)),
|
||||
({"state_classes": ["total", "beer"]}, pytest.raises(vol.Invalid)),
|
||||
({"state_classes": ["cat", "total"]}, pytest.raises(vol.Invalid)),
|
||||
],
|
||||
)
|
||||
def test_state_class_selector_validate_schema(
|
||||
schema: dict, raises: AbstractContextManager
|
||||
) -> None:
|
||||
"""Test state class selector schemas."""
|
||||
# Validate selector configuration
|
||||
|
||||
with raises:
|
||||
selector.validate_selector({"state_class": schema})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("schema", "valid_selections", "invalid_selections"),
|
||||
[
|
||||
(
|
||||
{},
|
||||
("measurement", "total", "total_increasing", "measurement_angle"),
|
||||
("cat", 0, None, ["measurement"]),
|
||||
),
|
||||
(
|
||||
None,
|
||||
("measurement", "total", "total_increasing", "measurement_angle"),
|
||||
("cat", 0, None, ["measurement"]),
|
||||
),
|
||||
(
|
||||
{"multiple": True},
|
||||
(["measurement"], ["total", "total_increasing", "measurement_angle"]),
|
||||
("measurement", 0, None, ["cat"]),
|
||||
),
|
||||
(
|
||||
{
|
||||
"state_classes": ["measurement", "total", "total_increasing"],
|
||||
"multiple": True,
|
||||
},
|
||||
(["measurement"], ["total", "total_increasing"]),
|
||||
("measurement", 0, None, ["cat"], ["measurement_angle"]),
|
||||
),
|
||||
(
|
||||
{
|
||||
"state_classes": ["measurement", "total", "total_increasing"],
|
||||
},
|
||||
("measurement", "total", "total_increasing"),
|
||||
(["measurement"], 0, None, "dog", "measurement_angle"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_state_class_selector_schema(
|
||||
schema, valid_selections, invalid_selections
|
||||
) -> None:
|
||||
"""Test state class selector."""
|
||||
_test_selector("state_class", schema, valid_selections, invalid_selections)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("schema", "valid_selections", "invalid_selections"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user