Add config flow to statistics (#120496)
parent
30a3e9af2b
commit
3d5d4f8ddb
|
@ -1,6 +1,27 @@
|
|||
"""The statistics component."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
DOMAIN = "statistics"
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Statistics from a config entry."""
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Statistics config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
"""Config flow for statistics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaCommonFlowHandler,
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowError,
|
||||
SchemaFlowFormStep,
|
||||
)
|
||||
from homeassistant.helpers.selector import (
|
||||
BooleanSelector,
|
||||
DurationSelector,
|
||||
DurationSelectorConfig,
|
||||
EntitySelector,
|
||||
EntitySelectorConfig,
|
||||
NumberSelector,
|
||||
NumberSelectorConfig,
|
||||
NumberSelectorMode,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
TextSelector,
|
||||
)
|
||||
|
||||
from . import DOMAIN
|
||||
from .sensor import (
|
||||
CONF_KEEP_LAST_SAMPLE,
|
||||
CONF_MAX_AGE,
|
||||
CONF_PERCENTILE,
|
||||
CONF_PRECISION,
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE,
|
||||
CONF_STATE_CHARACTERISTIC,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_PRECISION,
|
||||
STATS_BINARY_SUPPORT,
|
||||
STATS_NUMERIC_SUPPORT,
|
||||
)
|
||||
|
||||
|
||||
async def get_state_characteristics(handler: SchemaCommonFlowHandler) -> vol.Schema:
|
||||
"""Return schema with state characteristics."""
|
||||
is_binary = (
|
||||
split_entity_id(handler.options[CONF_ENTITY_ID])[0] == BINARY_SENSOR_DOMAIN
|
||||
)
|
||||
if is_binary:
|
||||
options = STATS_BINARY_SUPPORT
|
||||
else:
|
||||
options = STATS_NUMERIC_SUPPORT
|
||||
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_STATE_CHARACTERISTIC): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=list(options),
|
||||
translation_key=CONF_STATE_CHARACTERISTIC,
|
||||
sort=True,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_options(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Validate options selected."""
|
||||
if (
|
||||
user_input.get(CONF_SAMPLES_MAX_BUFFER_SIZE) is None
|
||||
and user_input.get(CONF_MAX_AGE) is None
|
||||
):
|
||||
raise SchemaFlowError("missing_max_age_or_sampling_size")
|
||||
|
||||
if (
|
||||
user_input.get(CONF_KEEP_LAST_SAMPLE) is True
|
||||
and user_input.get(CONF_MAX_AGE) is None
|
||||
):
|
||||
raise SchemaFlowError("missing_keep_last_sample")
|
||||
|
||||
handler.parent_handler._async_abort_entries_match({**handler.options, **user_input}) # noqa: SLF001
|
||||
|
||||
return user_input
|
||||
|
||||
|
||||
DATA_SCHEMA_SETUP = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME, default=DEFAULT_NAME): TextSelector(),
|
||||
vol.Required(CONF_ENTITY_ID): EntitySelector(
|
||||
EntitySelectorConfig(domain=[BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN])
|
||||
),
|
||||
}
|
||||
)
|
||||
DATA_SCHEMA_OPTIONS = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_SAMPLES_MAX_BUFFER_SIZE): NumberSelector(
|
||||
NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX)
|
||||
),
|
||||
vol.Optional(CONF_MAX_AGE): DurationSelector(
|
||||
DurationSelectorConfig(enable_day=False, allow_negative=False)
|
||||
),
|
||||
vol.Optional(CONF_KEEP_LAST_SAMPLE, default=False): BooleanSelector(),
|
||||
vol.Optional(CONF_PERCENTILE, default=50): NumberSelector(
|
||||
NumberSelectorConfig(min=1, max=99, step=1, mode=NumberSelectorMode.BOX)
|
||||
),
|
||||
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): NumberSelector(
|
||||
NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_FLOW = {
|
||||
"user": SchemaFlowFormStep(
|
||||
schema=DATA_SCHEMA_SETUP,
|
||||
next_step="state_characteristic",
|
||||
),
|
||||
"state_characteristic": SchemaFlowFormStep(
|
||||
schema=get_state_characteristics, next_step="options"
|
||||
),
|
||||
"options": SchemaFlowFormStep(
|
||||
schema=DATA_SCHEMA_OPTIONS,
|
||||
validate_user_input=validate_options,
|
||||
),
|
||||
}
|
||||
OPTIONS_FLOW = {
|
||||
"init": SchemaFlowFormStep(
|
||||
DATA_SCHEMA_OPTIONS,
|
||||
validate_user_input=validate_options,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config flow for Statistics."""
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
return cast(str, options[CONF_NAME])
|
|
@ -3,7 +3,9 @@
|
|||
"name": "Statistics",
|
||||
"after_dependencies": ["recorder"],
|
||||
"codeowners": ["@ThomDietrich"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/statistics",
|
||||
"integration_type": "helper",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ from homeassistant.components.sensor import (
|
|||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
|
@ -282,6 +283,42 @@ async def async_setup_platform(
|
|||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Statistics sensor entry."""
|
||||
sampling_size = entry.options.get(CONF_SAMPLES_MAX_BUFFER_SIZE)
|
||||
if sampling_size:
|
||||
sampling_size = int(sampling_size)
|
||||
|
||||
max_age = None
|
||||
if max_age_input := entry.options.get(CONF_MAX_AGE):
|
||||
max_age = timedelta(
|
||||
hours=max_age_input["hours"],
|
||||
minutes=max_age_input["minutes"],
|
||||
seconds=max_age_input["seconds"],
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
StatisticsSensor(
|
||||
source_entity_id=entry.options[CONF_ENTITY_ID],
|
||||
name=entry.options[CONF_NAME],
|
||||
unique_id=entry.entry_id,
|
||||
state_characteristic=entry.options[CONF_STATE_CHARACTERISTIC],
|
||||
samples_max_buffer_size=sampling_size,
|
||||
samples_max_age=max_age,
|
||||
samples_keep_last=entry.options[CONF_KEEP_LAST_SAMPLE],
|
||||
precision=int(entry.options[CONF_PRECISION]),
|
||||
percentile=int(entry.options[CONF_PERCENTILE]),
|
||||
)
|
||||
],
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class StatisticsSensor(SensorEntity):
|
||||
"""Representation of a Statistics sensor."""
|
||||
|
||||
|
|
|
@ -1,4 +1,115 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
},
|
||||
"error": {
|
||||
"missing_max_age_or_sampling_size": "The sensor configuration must provide 'max_age' and/or 'sampling_size'",
|
||||
"missing_keep_last_sample": "The sensor configuration must provide 'max_age' if 'keep_last_sample' is True"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Add a statistics sensor",
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"entity_id": "Entity"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "Name for the created entity.",
|
||||
"entity_id": "Entity to get statistics from."
|
||||
}
|
||||
},
|
||||
"state_characteristic": {
|
||||
"description": "Read the documention for further details on available options and how to use them.",
|
||||
"data": {
|
||||
"state_characteristic": "State_characteristic"
|
||||
},
|
||||
"data_description": {
|
||||
"state_characteristic": "The characteristic that should be used as the state of the statistics sensor."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"description": "Read the documention for further details on how to configure the statistics sensor using these options.",
|
||||
"data": {
|
||||
"sampling_size": "Sampling size",
|
||||
"max_age": "Max age",
|
||||
"keep_last_sample": "Keep last sample",
|
||||
"percentile": "Percentile",
|
||||
"precision": "Precision"
|
||||
},
|
||||
"data_description": {
|
||||
"sampling_size": "Maximum number of source sensor measurements stored.",
|
||||
"max_age": "Maximum age of source sensor measurements stored.",
|
||||
"keep_last_sample": "Defines whether the most recent sampled value should be preserved regardless of the 'max age' setting.",
|
||||
"percentile": "Only relevant in combination with the 'percentile' characteristic. Must be a value between 1 and 99.",
|
||||
"precision": "Defines the number of decimal places of the calculated sensor value."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
},
|
||||
"error": {
|
||||
"missing_max_age_or_sampling_size": "[%key:component::statistics::config::error::missing_max_age_or_sampling_size%]",
|
||||
"missing_keep_last_sample": "[%key:component::statistics::config::error::missing_keep_last_sample%]"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "[%key:component::statistics::config::step::options::description%]",
|
||||
"data": {
|
||||
"sampling_size": "[%key:component::statistics::config::step::options::data::sampling_size%]",
|
||||
"max_age": "[%key:component::statistics::config::step::options::data::max_age%]",
|
||||
"keep_last_sample": "[%key:component::statistics::config::step::options::data::keep_last_sample%]",
|
||||
"percentile": "[%key:component::statistics::config::step::options::data::percentile%]",
|
||||
"precision": "[%key:component::statistics::config::step::options::data::precision%]"
|
||||
},
|
||||
"data_description": {
|
||||
"sampling_size": "[%key:component::statistics::config::step::options::data_description::sampling_size%]",
|
||||
"max_age": "[%key:component::statistics::config::step::options::data_description::max_age%]",
|
||||
"keep_last_sample": "[%key:component::statistics::config::step::options::data_description::keep_last_sample%]",
|
||||
"percentile": "[%key:component::statistics::config::step::options::data_description::percentile%]",
|
||||
"precision": "[%key:component::statistics::config::step::options::data_description::precision%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"state_characteristic": {
|
||||
"options": {
|
||||
"average_linear": "Average linear",
|
||||
"average_step": "Average step",
|
||||
"average_timeless": "Average timeless",
|
||||
"change": "Change",
|
||||
"change_sample": "Change sample",
|
||||
"change_second": "Change second",
|
||||
"count": "Count",
|
||||
"count_on": "Count on",
|
||||
"count_off": "Count off",
|
||||
"datetime_newest": "Newest datetime",
|
||||
"datetime_oldest": "Oldest datetime",
|
||||
"datetime_value_max": "Max value datetime",
|
||||
"datetime_value_min": "Min value datetime",
|
||||
"distance_95_percent_of_values": "Distance 95% of values",
|
||||
"distance_99_percent_of_values": "Distance 99% of values",
|
||||
"distance_absolute": "Absolute distance",
|
||||
"mean": "Mean",
|
||||
"mean_circular": "Mean circular",
|
||||
"median": "Median",
|
||||
"noisiness": "Noisiness",
|
||||
"percentile": "Percentile",
|
||||
"standard_deviation": "Standard deviation",
|
||||
"sum": "Sum",
|
||||
"sum_differences": "Sum of differences",
|
||||
"sum_differences_nonnegative": "Sum of differences non-negative",
|
||||
"total": "Total",
|
||||
"value_max": "Max value",
|
||||
"value_min": "Min value",
|
||||
"variance": "Variance"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reload": {
|
||||
"name": "[%key:common::action::reload%]",
|
||||
|
|
|
@ -12,6 +12,7 @@ FLOWS = {
|
|||
"integration",
|
||||
"min_max",
|
||||
"random",
|
||||
"statistics",
|
||||
"switch_as_x",
|
||||
"template",
|
||||
"threshold",
|
||||
|
|
|
@ -5772,12 +5772,6 @@
|
|||
"config_flow": false,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"statistics": {
|
||||
"name": "Statistics",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"statsd": {
|
||||
"name": "StatsD",
|
||||
"integration_type": "hub",
|
||||
|
@ -7213,6 +7207,12 @@
|
|||
"integration_type": "helper",
|
||||
"config_flow": false
|
||||
},
|
||||
"statistics": {
|
||||
"name": "Statistics",
|
||||
"integration_type": "helper",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"switch_as_x": {
|
||||
"integration_type": "helper",
|
||||
"config_flow": true,
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
"""Fixtures for the Statistics integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.statistics import DOMAIN
|
||||
from homeassistant.components.statistics.sensor import (
|
||||
CONF_KEEP_LAST_SAMPLE,
|
||||
CONF_MAX_AGE,
|
||||
CONF_PERCENTILE,
|
||||
CONF_PRECISION,
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE,
|
||||
CONF_STATE_CHARACTERISTIC,
|
||||
DEFAULT_NAME,
|
||||
STAT_AVERAGE_LINEAR,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_NAME,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .test_sensor import VALUES_NUMERIC
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Automatically path uuid generator."""
|
||||
with patch(
|
||||
"homeassistant.components.statistics.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture(name="get_config")
|
||||
async def get_config_to_integration_load() -> dict[str, Any]:
|
||||
"""Return configuration.
|
||||
|
||||
To override the config, tests can be marked with:
|
||||
@pytest.mark.parametrize("get_config", [{...}])
|
||||
"""
|
||||
return {
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_ENTITY_ID: "sensor.test_monitored",
|
||||
CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR,
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0,
|
||||
CONF_MAX_AGE: {"hours": 8, "minutes": 5, "seconds": 5},
|
||||
CONF_KEEP_LAST_SAMPLE: False,
|
||||
CONF_PERCENTILE: 50.0,
|
||||
CONF_PRECISION: 2.0,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(name="loaded_entry")
|
||||
async def load_integration(
|
||||
hass: HomeAssistant, get_config: dict[str, Any]
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the Statistics integration in Home Assistant."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
source=SOURCE_USER,
|
||||
options=get_config,
|
||||
entry_id="1",
|
||||
)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for value in VALUES_NUMERIC:
|
||||
hass.states.async_set(
|
||||
"sensor.test_monitored",
|
||||
str(value),
|
||||
{ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return config_entry
|
|
@ -0,0 +1,273 @@
|
|||
"""Test the Scrape config flow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.statistics import DOMAIN
|
||||
from homeassistant.components.statistics.sensor import (
|
||||
CONF_KEEP_LAST_SAMPLE,
|
||||
CONF_MAX_AGE,
|
||||
CONF_PERCENTILE,
|
||||
CONF_PRECISION,
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE,
|
||||
CONF_STATE_CHARACTERISTIC,
|
||||
DEFAULT_NAME,
|
||||
STAT_AVERAGE_LINEAR,
|
||||
STAT_COUNT,
|
||||
)
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_form_sensor(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
|
||||
"""Test we get the form for sensor."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_ENTITY_ID: "sensor.test_monitored",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0,
|
||||
CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["version"] == 1
|
||||
assert result["options"] == {
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_ENTITY_ID: "sensor.test_monitored",
|
||||
CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR,
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0,
|
||||
CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0},
|
||||
CONF_KEEP_LAST_SAMPLE: False,
|
||||
CONF_PERCENTILE: 50.0,
|
||||
CONF_PRECISION: 2.0,
|
||||
}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_binary_sensor(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test we get the form for binary sensor."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_ENTITY_ID: "binary_sensor.test_monitored",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_STATE_CHARACTERISTIC: STAT_COUNT,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0,
|
||||
CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["version"] == 1
|
||||
assert result["options"] == {
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_ENTITY_ID: "binary_sensor.test_monitored",
|
||||
CONF_STATE_CHARACTERISTIC: STAT_COUNT,
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0,
|
||||
CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0},
|
||||
CONF_KEEP_LAST_SAMPLE: False,
|
||||
CONF_PERCENTILE: 50.0,
|
||||
CONF_PRECISION: 2.0,
|
||||
}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_options_flow(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None:
|
||||
"""Test options flow."""
|
||||
|
||||
result = await hass.config_entries.options.async_init(loaded_entry.entry_id)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE: 25.0,
|
||||
CONF_MAX_AGE: {"hours": 16, "minutes": 0, "seconds": 0},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_ENTITY_ID: "sensor.test_monitored",
|
||||
CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR,
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE: 25.0,
|
||||
CONF_MAX_AGE: {"hours": 16, "minutes": 0, "seconds": 0},
|
||||
CONF_KEEP_LAST_SAMPLE: False,
|
||||
CONF_PERCENTILE: 50.0,
|
||||
CONF_PRECISION: 2.0,
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check the entity was updated, no new entity was created
|
||||
assert len(hass.states.async_all()) == 2
|
||||
|
||||
state = hass.states.get("sensor.statistical_characteristic")
|
||||
assert state is not None
|
||||
|
||||
|
||||
async def test_validation_options(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test validation."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_ENTITY_ID: "sensor.test_monitored",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["step_id"] == "options"
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "missing_max_age_or_sampling_size"}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_KEEP_LAST_SAMPLE: True, CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["step_id"] == "options"
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "missing_keep_last_sample"}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0,
|
||||
CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["version"] == 1
|
||||
assert result["options"] == {
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_ENTITY_ID: "sensor.test_monitored",
|
||||
CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR,
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0,
|
||||
CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0},
|
||||
CONF_KEEP_LAST_SAMPLE: False,
|
||||
CONF_PERCENTILE: 50.0,
|
||||
CONF_PRECISION: 2.0,
|
||||
}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_entry_already_exist(
|
||||
hass: HomeAssistant, loaded_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test abort when entry already exist."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_ENTITY_ID: "sensor.test_monitored",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0,
|
||||
CONF_MAX_AGE: {"hours": 8, "minutes": 5, "seconds": 5},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
|
@ -0,0 +1,17 @@
|
|||
"""Test Statistics component setup process."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None:
|
||||
"""Test unload an entry."""
|
||||
|
||||
assert loaded_entry.state is ConfigEntryState.LOADED
|
||||
assert await hass.config_entries.async_unload(loaded_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert loaded_entry.state is ConfigEntryState.NOT_LOADED
|
|
@ -19,10 +19,20 @@ from homeassistant.components.sensor import (
|
|||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN
|
||||
from homeassistant.components.statistics.sensor import StatisticsSensor
|
||||
from homeassistant.components.statistics.sensor import (
|
||||
CONF_KEEP_LAST_SAMPLE,
|
||||
CONF_PERCENTILE,
|
||||
CONF_PRECISION,
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE,
|
||||
CONF_STATE_CHARACTERISTIC,
|
||||
STAT_MEAN,
|
||||
StatisticsSensor,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_NAME,
|
||||
DEGREE,
|
||||
SERVICE_RELOAD,
|
||||
STATE_UNAVAILABLE,
|
||||
|
@ -35,7 +45,7 @@ from homeassistant.helpers import entity_registry as er
|
|||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import async_fire_time_changed, get_fixture_path
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, get_fixture_path
|
||||
from tests.components.recorder.common import async_wait_recording_done
|
||||
|
||||
VALUES_BINARY = ["on", "off", "on", "off", "on", "off", "on", "off", "on"]
|
||||
|
@ -171,6 +181,35 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant) -> None:
|
|||
assert new_state.attributes.get("source_value_valid") is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"get_config",
|
||||
[
|
||||
{
|
||||
CONF_NAME: "test",
|
||||
CONF_ENTITY_ID: "sensor.test_monitored",
|
||||
CONF_STATE_CHARACTERISTIC: STAT_MEAN,
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0,
|
||||
CONF_KEEP_LAST_SAMPLE: False,
|
||||
CONF_PERCENTILE: 50.0,
|
||||
CONF_PRECISION: 2.0,
|
||||
}
|
||||
],
|
||||
)
|
||||
async def test_sensor_loaded_from_config_entry(
|
||||
hass: HomeAssistant, loaded_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test the sensor loaded from a config entry."""
|
||||
|
||||
state = hass.states.get("sensor.test")
|
||||
assert state is not None
|
||||
assert state.state == str(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2))
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
|
||||
assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2)
|
||||
assert state.attributes.get("source_value_valid") is True
|
||||
assert "age_coverage_ratio" not in state.attributes
|
||||
|
||||
|
||||
async def test_sensor_defaults_binary(hass: HomeAssistant) -> None:
|
||||
"""Test the general behavior of the sensor, with binary source sensor."""
|
||||
assert await async_setup_component(
|
||||
|
|
Loading…
Reference in New Issue