Add config flow to statistics (#120496)

pull/120571/head
G Johansson 2024-06-26 16:06:35 +02:00 committed by GitHub
parent 30a3e9af2b
commit 3d5d4f8ddb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 749 additions and 8 deletions

View File

@ -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)

View File

@ -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])

View File

@ -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"
}

View File

@ -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."""

View File

@ -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%]",

View File

@ -12,6 +12,7 @@ FLOWS = {
"integration",
"min_max",
"random",
"statistics",
"switch_as_x",
"template",
"threshold",

View File

@ -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,

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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(