mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 00:03:16 +01:00
Allow history_stats to configure state_class: total_increasing (#148637)
This commit is contained in:
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS, SensorStateClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_STATE
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -105,6 +106,12 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, options=options, minor_version=2
|
||||
)
|
||||
if config_entry.minor_version < 3:
|
||||
# Set the state class to measurement for backward compatibility
|
||||
options[CONF_STATE_CLASS] = SensorStateClass.MEASUREMENT
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, options=options, minor_version=3
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s successful",
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any, cast
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS, SensorStateClass
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_STATE, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -39,6 +40,7 @@ from .const import (
|
||||
CONF_PERIOD_KEYS,
|
||||
CONF_START,
|
||||
CONF_TYPE_KEYS,
|
||||
CONF_TYPE_RATIO,
|
||||
CONF_TYPE_TIME,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
@@ -101,10 +103,19 @@ async def get_state_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
|
||||
async def get_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
|
||||
"""Return schema for options step."""
|
||||
entity_id = handler.options[CONF_ENTITY_ID]
|
||||
return _get_options_schema_with_entity_id(entity_id)
|
||||
conf_type = handler.options[CONF_TYPE]
|
||||
return _get_options_schema_with_entity_id(entity_id, conf_type)
|
||||
|
||||
|
||||
def _get_options_schema_with_entity_id(entity_id: str) -> vol.Schema:
|
||||
def _get_options_schema_with_entity_id(entity_id: str, type: str) -> vol.Schema:
|
||||
state_class_options = (
|
||||
[SensorStateClass.MEASUREMENT]
|
||||
if type == CONF_TYPE_RATIO
|
||||
else [
|
||||
SensorStateClass.MEASUREMENT,
|
||||
SensorStateClass.TOTAL_INCREASING,
|
||||
]
|
||||
)
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ENTITY_ID): EntitySelector(
|
||||
@@ -130,6 +141,13 @@ def _get_options_schema_with_entity_id(entity_id: 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,
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -158,7 +176,7 @@ OPTIONS_FLOW = {
|
||||
class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config flow for History stats."""
|
||||
|
||||
MINOR_VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
@@ -201,6 +219,7 @@ async def ws_start_preview(
|
||||
config_entry = hass.config_entries.async_get_entry(flow_status["handler"])
|
||||
entity_id = options[CONF_ENTITY_ID]
|
||||
name = options[CONF_NAME]
|
||||
conf_type = options[CONF_TYPE]
|
||||
else:
|
||||
flow_status = hass.config_entries.options.async_get(msg["flow_id"])
|
||||
config_entry = hass.config_entries.async_get_entry(flow_status["handler"])
|
||||
@@ -208,6 +227,7 @@ async def ws_start_preview(
|
||||
raise HomeAssistantError("Config entry not found")
|
||||
entity_id = config_entry.options[CONF_ENTITY_ID]
|
||||
name = config_entry.options[CONF_NAME]
|
||||
conf_type = config_entry.options[CONF_TYPE]
|
||||
|
||||
@callback
|
||||
def async_preview_updated(
|
||||
@@ -233,7 +253,7 @@ async def ws_start_preview(
|
||||
|
||||
validated_data: Any = None
|
||||
try:
|
||||
validated_data = (_get_options_schema_with_entity_id(entity_id))(
|
||||
validated_data = (_get_options_schema_with_entity_id(entity_id, conf_type))(
|
||||
msg["user_input"]
|
||||
)
|
||||
except vol.Invalid as ex:
|
||||
@@ -255,6 +275,7 @@ async def ws_start_preview(
|
||||
start = validated_data.get(CONF_START)
|
||||
end = validated_data.get(CONF_END)
|
||||
duration = validated_data.get(CONF_DURATION)
|
||||
state_class = validated_data.get(CONF_STATE_CLASS)
|
||||
|
||||
history_stats = HistoryStats(
|
||||
hass,
|
||||
@@ -274,6 +295,7 @@ async def ws_start_preview(
|
||||
name=name,
|
||||
unique_id=None,
|
||||
source_entity_id=entity_id,
|
||||
state_class=state_class,
|
||||
)
|
||||
preview_entity.hass = hass
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
@@ -72,6 +73,16 @@ def exactly_two_period_keys[_T: dict[str, Any]](conf: _T) -> _T:
|
||||
return conf
|
||||
|
||||
|
||||
def no_ratio_total[_T: dict[str, Any]](conf: _T) -> _T:
|
||||
"""Ensure state_class:total_increasing not used with type:ratio."""
|
||||
if (
|
||||
conf.get(CONF_TYPE) == CONF_TYPE_RATIO
|
||||
and conf.get(CONF_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING
|
||||
):
|
||||
raise vol.Invalid("State class total_increasing not to be used with type ratio")
|
||||
return conf
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
@@ -83,9 +94,15 @@ PLATFORM_SCHEMA = vol.All(
|
||||
vol.Optional(CONF_TYPE, default=CONF_TYPE_TIME): vol.In(CONF_TYPE_KEYS),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(
|
||||
CONF_STATE_CLASS, default=SensorStateClass.MEASUREMENT
|
||||
): vol.In(
|
||||
[None, SensorStateClass.MEASUREMENT, SensorStateClass.TOTAL_INCREASING]
|
||||
),
|
||||
}
|
||||
),
|
||||
exactly_two_period_keys,
|
||||
no_ratio_total,
|
||||
)
|
||||
|
||||
|
||||
@@ -106,6 +123,9 @@ async def async_setup_platform(
|
||||
sensor_type: str = config[CONF_TYPE]
|
||||
name: str = config[CONF_NAME]
|
||||
unique_id: str | None = config.get(CONF_UNIQUE_ID)
|
||||
state_class: SensorStateClass | None = config.get(
|
||||
CONF_STATE_CLASS, SensorStateClass.MEASUREMENT
|
||||
)
|
||||
|
||||
history_stats = HistoryStats(hass, entity_id, entity_states, start, end, duration)
|
||||
coordinator = HistoryStatsUpdateCoordinator(hass, history_stats, None, name)
|
||||
@@ -121,6 +141,7 @@ async def async_setup_platform(
|
||||
name=name,
|
||||
unique_id=unique_id,
|
||||
source_entity_id=entity_id,
|
||||
state_class=state_class,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -136,6 +157,7 @@ async def async_setup_entry(
|
||||
sensor_type: str = entry.options[CONF_TYPE]
|
||||
coordinator = entry.runtime_data
|
||||
entity_id: str = entry.options[CONF_ENTITY_ID]
|
||||
state_class: SensorStateClass | None = entry.options.get(CONF_STATE_CLASS)
|
||||
async_add_entities(
|
||||
[
|
||||
HistoryStatsSensor(
|
||||
@@ -145,6 +167,7 @@ async def async_setup_entry(
|
||||
name=entry.title,
|
||||
unique_id=entry.entry_id,
|
||||
source_entity_id=entity_id,
|
||||
state_class=state_class,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -185,8 +208,6 @@ class HistoryStatsSensorBase(
|
||||
class HistoryStatsSensor(HistoryStatsSensorBase):
|
||||
"""A HistoryStats sensor."""
|
||||
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
@@ -196,6 +217,7 @@ class HistoryStatsSensor(HistoryStatsSensorBase):
|
||||
name: str,
|
||||
unique_id: str | None,
|
||||
source_entity_id: str,
|
||||
state_class: SensorStateClass | None,
|
||||
) -> None:
|
||||
"""Initialize the HistoryStats sensor."""
|
||||
super().__init__(coordinator, name)
|
||||
@@ -204,6 +226,7 @@ class HistoryStatsSensor(HistoryStatsSensorBase):
|
||||
) = None
|
||||
self._attr_native_unit_of_measurement = UNITS[sensor_type]
|
||||
self._type = sensor_type
|
||||
self._attr_state_class = state_class
|
||||
self._attr_unique_id = unique_id
|
||||
if source_entity_id: # Guard against empty source_entity_id in preview mode
|
||||
self.device_entry = async_entity_id_to_device(
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"entity_id": "[%key:component::history_stats::config::step::user::data::entity_id%]",
|
||||
"start": "Start",
|
||||
"state": "[%key:component::history_stats::config::step::user::data::state%]",
|
||||
"state_class": "[%key:component::sensor::entity_component::_::state_attributes::state_class::name%]",
|
||||
"type": "[%key:component::history_stats::config::step::user::data::type%]"
|
||||
},
|
||||
"data_description": {
|
||||
@@ -22,6 +23,7 @@
|
||||
"entity_id": "[%key:component::history_stats::config::step::user::data_description::entity_id%]",
|
||||
"start": "When to start the measure (timestamp or datetime). Can be a template.",
|
||||
"state": "[%key:component::history_stats::config::step::user::data_description::state%]",
|
||||
"state_class": "The state class for statistics calculation.",
|
||||
"type": "[%key:component::history_stats::config::step::user::data_description::type%]"
|
||||
},
|
||||
"description": "Read the documentation for further details on how to configure the history stats sensor using these options."
|
||||
@@ -68,6 +70,7 @@
|
||||
"entity_id": "[%key:component::history_stats::config::step::user::data::entity_id%]",
|
||||
"start": "[%key:component::history_stats::config::step::options::data::start%]",
|
||||
"state": "[%key:component::history_stats::config::step::user::data::state%]",
|
||||
"state_class": "[%key:component::sensor::entity_component::_::state_attributes::state_class::name%]",
|
||||
"type": "[%key:component::history_stats::config::step::user::data::type%]"
|
||||
},
|
||||
"data_description": {
|
||||
@@ -76,6 +79,7 @@
|
||||
"entity_id": "[%key:component::history_stats::config::step::user::data_description::entity_id%]",
|
||||
"start": "[%key:component::history_stats::config::step::options::data_description::start%]",
|
||||
"state": "[%key:component::history_stats::config::step::user::data_description::state%]",
|
||||
"state_class": "The state class for statistics calculation. Changing the state class will require statistics to be reset.",
|
||||
"type": "[%key:component::history_stats::config::step::user::data_description::type%]"
|
||||
},
|
||||
"description": "[%key:component::history_stats::config::step::options::description%]"
|
||||
@@ -83,6 +87,12 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.components.history_stats.const import (
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_STATE, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
@@ -48,6 +49,7 @@ async def get_config_to_integration_load() -> dict[str, Any]:
|
||||
CONF_TYPE: "count",
|
||||
CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}",
|
||||
CONF_END: "{{ utcnow() }}",
|
||||
CONF_STATE_CLASS: "measurement",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.components.history_stats.const import (
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_STATE, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@@ -91,6 +92,7 @@ async def test_options_flow(
|
||||
user_input={
|
||||
CONF_END: "{{ utcnow() }}",
|
||||
CONF_DURATION: {"hours": 8, "minutes": 0, "seconds": 0, "days": 20},
|
||||
CONF_STATE_CLASS: "total_increasing",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@@ -103,6 +105,7 @@ async def test_options_flow(
|
||||
CONF_TYPE: "count",
|
||||
CONF_END: "{{ utcnow() }}",
|
||||
CONF_DURATION: {"hours": 8, "minutes": 0, "seconds": 0, "days": 20},
|
||||
CONF_STATE_CLASS: "total_increasing",
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
@@ -387,6 +390,7 @@ async def test_options_flow_preview(
|
||||
CONF_STATE: ["on"],
|
||||
CONF_END: "{{ now() }}",
|
||||
CONF_START: "{{ today_at() }}",
|
||||
CONF_STATE_CLASS: "measurement",
|
||||
},
|
||||
title=DEFAULT_NAME,
|
||||
)
|
||||
@@ -422,6 +426,7 @@ async def test_options_flow_preview(
|
||||
CONF_STATE: ["on"],
|
||||
CONF_END: end,
|
||||
CONF_START: "{{ today_at() }}",
|
||||
CONF_STATE_CLASS: "measurement",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.components.history_stats.const import (
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS, SensorStateClass
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_STATE, CONF_TYPE
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
@@ -419,7 +420,58 @@ async def test_migration_1_1(
|
||||
assert history_stats_entity_entry.device_id == sensor_entity_entry.device_id
|
||||
|
||||
assert history_stats_config_entry.version == 1
|
||||
assert history_stats_config_entry.minor_version == 2
|
||||
assert (
|
||||
history_stats_config_entry.minor_version
|
||||
== HistoryStatsConfigFlowHandler.MINOR_VERSION
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_migration_1_2(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
sensor_entity_entry: er.RegistryEntry,
|
||||
sensor_device: dr.DeviceEntry,
|
||||
) -> None:
|
||||
"""Test migration from v1.2 sets state_class to measurement."""
|
||||
|
||||
history_stats_config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_ENTITY_ID: sensor_entity_entry.entity_id,
|
||||
CONF_STATE: ["on"],
|
||||
CONF_TYPE: "count",
|
||||
CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}",
|
||||
CONF_END: "{{ utcnow() }}",
|
||||
},
|
||||
title="My history stats",
|
||||
version=1,
|
||||
minor_version=2,
|
||||
)
|
||||
history_stats_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(history_stats_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert history_stats_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert (
|
||||
history_stats_config_entry.options.get(CONF_STATE_CLASS)
|
||||
== SensorStateClass.MEASUREMENT
|
||||
)
|
||||
assert history_stats_config_entry.version == 1
|
||||
assert (
|
||||
history_stats_config_entry.minor_version
|
||||
== HistoryStatsConfigFlowHandler.MINOR_VERSION
|
||||
)
|
||||
|
||||
assert hass.states.get("sensor.my_history_stats") is not None
|
||||
assert (
|
||||
hass.states.get("sensor.my_history_stats").attributes.get(CONF_STATE_CLASS)
|
||||
== SensorStateClass.MEASUREMENT
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
|
||||
@@ -120,6 +120,16 @@ async def test_setup_multiple_states(
|
||||
"end": "{{ utcnow() }}",
|
||||
"duration": "01:00",
|
||||
},
|
||||
{
|
||||
"platform": "history_stats",
|
||||
"entity_id": "binary_sensor.test_id",
|
||||
"name": "Test",
|
||||
"state": "on",
|
||||
"start": "{{ as_timestamp(utcnow()) - 3600 }}",
|
||||
"end": "{{ utcnow() }}",
|
||||
"type": "ratio",
|
||||
"state_class": "total_increasing",
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("hass")
|
||||
@@ -321,6 +331,7 @@ async def test_measure_multiple(recorder_mock: Recorder, hass: HomeAssistant) ->
|
||||
"start": "{{ as_timestamp(utcnow()) - 3600 }}",
|
||||
"end": "{{ utcnow() }}",
|
||||
"type": "time",
|
||||
"state_class": "measurement",
|
||||
},
|
||||
{
|
||||
"platform": "history_stats",
|
||||
@@ -330,6 +341,7 @@ async def test_measure_multiple(recorder_mock: Recorder, hass: HomeAssistant) ->
|
||||
"start": "{{ as_timestamp(utcnow()) - 3600 }}",
|
||||
"end": "{{ utcnow() }}",
|
||||
"type": "time",
|
||||
"state_class": "total_increasing",
|
||||
},
|
||||
{
|
||||
"platform": "history_stats",
|
||||
@@ -362,6 +374,20 @@ async def test_measure_multiple(recorder_mock: Recorder, hass: HomeAssistant) ->
|
||||
assert hass.states.get("sensor.sensor3").state == "2"
|
||||
assert hass.states.get("sensor.sensor4").state == "50.0"
|
||||
|
||||
assert (
|
||||
hass.states.get("sensor.sensor1").attributes.get("state_class") == "measurement"
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.sensor2").attributes.get("state_class")
|
||||
== "total_increasing"
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.sensor3").attributes.get("state_class") == "measurement"
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.sensor4").attributes.get("state_class") == "measurement"
|
||||
)
|
||||
|
||||
|
||||
async def test_measure(recorder_mock: Recorder, hass: HomeAssistant) -> None:
|
||||
"""Test the history statistics sensor measure."""
|
||||
|
||||
Reference in New Issue
Block a user