mirror of
https://github.com/elisspace/core.git
synced 2026-08-31 00:22:12 +00:00
Add config flow to the statistics integration
This commit is contained in:
@@ -1,6 +1,26 @@
|
||||
"""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 Min/Max from a config entry."""
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
214
homeassistant/components/statistics/config_flow.py
Normal file
214
homeassistant/components/statistics/config_flow.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""Config flow for Min/Max integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import CONF_ENTITY_ID
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaCommonFlowHandler,
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
SchemaFlowMenuStep,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_PRECISION,
|
||||
CONF_STATE_CHARACTERISTIC,
|
||||
DOMAIN,
|
||||
STAT_AVERAGE_STEP_LTS,
|
||||
STAT_SUM_DIFFERENCES_LTS,
|
||||
STAT_VALUE_MAX_LTS,
|
||||
STAT_VALUE_MIN_LTS,
|
||||
)
|
||||
|
||||
_CALENDAR_PERIODS = [
|
||||
selector.SelectOptionDict(value="hour", label="Hour"),
|
||||
selector.SelectOptionDict(value="day", label="Day"),
|
||||
selector.SelectOptionDict(value="week", label="Week"),
|
||||
selector.SelectOptionDict(value="month", label="Month"),
|
||||
selector.SelectOptionDict(value="year", label="Year"),
|
||||
]
|
||||
|
||||
_STATISTIC_CHARACTERISTICS = [
|
||||
selector.SelectOptionDict(value=STAT_VALUE_MIN_LTS, label="Minimum"),
|
||||
selector.SelectOptionDict(value=STAT_VALUE_MAX_LTS, label="Maximum"),
|
||||
selector.SelectOptionDict(value=STAT_AVERAGE_STEP_LTS, label="Arithmetic mean"),
|
||||
selector.SelectOptionDict(
|
||||
value=STAT_SUM_DIFFERENCES_LTS, label="Sum of differences (change)"
|
||||
),
|
||||
]
|
||||
|
||||
PERIOD_TYPES = [
|
||||
"calendar",
|
||||
"fixed_period",
|
||||
"rolling_window",
|
||||
]
|
||||
|
||||
OPTIONS_SCHEMA_STEP_1 = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=SENSOR_DOMAIN),
|
||||
),
|
||||
vol.Required(CONF_STATE_CHARACTERISTIC): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(options=_STATISTIC_CHARACTERISTICS),
|
||||
),
|
||||
vol.Required(CONF_PRECISION, default=2): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0, max=6, mode=selector.NumberSelectorMode.BOX
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA_STEP_1 = vol.Schema(
|
||||
{
|
||||
vol.Required("name"): selector.TextSelector(),
|
||||
}
|
||||
).extend(OPTIONS_SCHEMA_STEP_1.schema)
|
||||
|
||||
CALENDAR_PERIOD_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("calendar_period"): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(options=_CALENDAR_PERIODS),
|
||||
),
|
||||
vol.Required("calendar_offset", default=0): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(min=0, mode=selector.NumberSelectorMode.BOX),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
FIXED_PERIOD_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("fixed_period_start_time"): selector.DateTimeSelector(),
|
||||
vol.Required("fixed_period_end_time"): selector.DateTimeSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
ROLLING_WINDOW_PERIOD_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("rolling_window_duration"): selector.DurationSelector(
|
||||
selector.DurationSelectorConfig(enable_day=True)
|
||||
),
|
||||
vol.Required("rolling_window_offset"): selector.DurationSelector(
|
||||
selector.DurationSelectorConfig(enable_day=True)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def choose_initial_config_step(options: dict[str, Any]) -> str | None:
|
||||
"""Choose the initial config step."""
|
||||
return "step_1" if not options else None
|
||||
|
||||
|
||||
@callback
|
||||
def set_period_suggested_values(options: dict[str, Any]) -> str:
|
||||
"""Add suggested values for editing the period."""
|
||||
|
||||
if calendar_period := options["period"].get("calendar"):
|
||||
options["calendar_offset"] = calendar_period["offset"]
|
||||
options["calendar_period"] = calendar_period["period"]
|
||||
elif fixed_period := options["period"].get("fixed_period"):
|
||||
options["fixed_period_start_time"] = fixed_period["start_time"]
|
||||
options["fixed_period_end_time"] = fixed_period["end_time"]
|
||||
else: # rolling_window
|
||||
rolling_window_period = options["period"]["rolling_window"]
|
||||
options["rolling_window_duration"] = rolling_window_period["duration"]
|
||||
options["rolling_window_offset"] = rolling_window_period["offset"]
|
||||
|
||||
return "period_type"
|
||||
|
||||
|
||||
@callback
|
||||
def set_period(
|
||||
period_type: str,
|
||||
) -> Callable[[SchemaCommonFlowHandler, dict[str, Any]], dict[str, Any]]:
|
||||
"""Set period."""
|
||||
|
||||
@callback
|
||||
def _set_period_type(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Add period to user input."""
|
||||
# pylint: disable-next=protected-access
|
||||
handler._options.pop("calendar_offset", None)
|
||||
# pylint: disable-next=protected-access
|
||||
handler._options.pop("calendar_period", None)
|
||||
# pylint: disable-next=protected-access
|
||||
handler._options.pop("fixed_period_start_time", None)
|
||||
# pylint: disable-next=protected-access
|
||||
handler._options.pop("fixed_period_end_time", None)
|
||||
# pylint: disable-next=protected-access
|
||||
handler._options.pop("rolling_window_duration", None)
|
||||
# pylint: disable-next=protected-access
|
||||
handler._options.pop("rolling_window_offset", None)
|
||||
|
||||
if period_type == "calendar":
|
||||
period = {
|
||||
"calendar": {
|
||||
"offset": user_input.pop("calendar_offset"),
|
||||
"period": user_input.pop("calendar_period"),
|
||||
}
|
||||
}
|
||||
elif period_type == "fixed_period":
|
||||
period = {
|
||||
"fixed_period": {
|
||||
"start_time": user_input.pop("fixed_period_start_time"),
|
||||
"end_time": user_input.pop("fixed_period_end_time"),
|
||||
}
|
||||
}
|
||||
else: # period_type = rolling_window
|
||||
period = {
|
||||
"rolling_window": {
|
||||
"duration": user_input.pop("rolling_window_duration"),
|
||||
"offset": user_input.pop("rolling_window_offset"),
|
||||
}
|
||||
}
|
||||
user_input["period"] = period
|
||||
return user_input
|
||||
|
||||
return _set_period_type
|
||||
|
||||
|
||||
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"user": SchemaFlowFormStep(None, next_step=choose_initial_config_step),
|
||||
"step_1": SchemaFlowFormStep(
|
||||
CONFIG_SCHEMA_STEP_1, next_step=lambda _: "period_type"
|
||||
),
|
||||
"period_type": SchemaFlowMenuStep(PERIOD_TYPES),
|
||||
"calendar": SchemaFlowFormStep(CALENDAR_PERIOD_SCHEMA, set_period("calendar")),
|
||||
"fixed_period": SchemaFlowFormStep(FIXED_PERIOD_SCHEMA, set_period("fixed_period")),
|
||||
"rolling_window": SchemaFlowFormStep(
|
||||
ROLLING_WINDOW_PERIOD_SCHEMA, set_period("rolling_window")
|
||||
),
|
||||
}
|
||||
|
||||
OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"init": SchemaFlowFormStep(
|
||||
OPTIONS_SCHEMA_STEP_1, next_step=set_period_suggested_values
|
||||
),
|
||||
"period_type": SchemaFlowMenuStep(PERIOD_TYPES),
|
||||
"calendar": SchemaFlowFormStep(CALENDAR_PERIOD_SCHEMA, set_period("calendar")),
|
||||
"fixed_period": SchemaFlowFormStep(FIXED_PERIOD_SCHEMA, set_period("fixed_period")),
|
||||
"rolling_window": SchemaFlowFormStep(
|
||||
ROLLING_WINDOW_PERIOD_SCHEMA, set_period("rolling_window")
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config or options flow for Min/Max."""
|
||||
|
||||
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["name"]) if "name" in options else ""
|
||||
11
homeassistant/components/statistics/const.py
Normal file
11
homeassistant/components/statistics/const.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Constants for the statistics integration."""
|
||||
|
||||
DOMAIN = "statistics"
|
||||
|
||||
CONF_PRECISION = "precision"
|
||||
CONF_STATE_CHARACTERISTIC = "state_characteristic"
|
||||
|
||||
STAT_AVERAGE_STEP_LTS = "average_step_lts"
|
||||
STAT_VALUE_MAX_LTS = "value_max_lts"
|
||||
STAT_VALUE_MIN_LTS = "value_min_lts"
|
||||
STAT_SUM_DIFFERENCES_LTS = "sum_differences_lts"
|
||||
@@ -5,5 +5,7 @@
|
||||
"after_dependencies": ["recorder"],
|
||||
"codeowners": ["@fabaff", "@ThomDietrich"],
|
||||
"quality_scale": "internal",
|
||||
"iot_class": "local_polling"
|
||||
"iot_class": "local_polling",
|
||||
"config_flow": true,
|
||||
"integration_type": "helper"
|
||||
}
|
||||
|
||||
@@ -12,13 +12,24 @@ from typing import Any, Literal, cast
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.recorder import get_instance, history
|
||||
from homeassistant.components.recorder import (
|
||||
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
|
||||
get_instance,
|
||||
history,
|
||||
)
|
||||
from homeassistant.components.recorder.models import StatisticPeriod
|
||||
from homeassistant.components.recorder.statistics import (
|
||||
get_metadata,
|
||||
statistic_during_period,
|
||||
)
|
||||
from homeassistant.components.recorder.util import PERIOD_SCHEMA, resolve_period
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
@@ -37,7 +48,7 @@ from homeassistant.core import (
|
||||
callback,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_point_in_utc_time,
|
||||
@@ -48,7 +59,16 @@ from homeassistant.helpers.start import async_at_start
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import DOMAIN, PLATFORMS
|
||||
from . import PLATFORMS
|
||||
from .const import (
|
||||
CONF_PRECISION,
|
||||
CONF_STATE_CHARACTERISTIC,
|
||||
DOMAIN,
|
||||
STAT_AVERAGE_STEP_LTS,
|
||||
STAT_SUM_DIFFERENCES_LTS,
|
||||
STAT_VALUE_MAX_LTS,
|
||||
STAT_VALUE_MIN_LTS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -87,6 +107,8 @@ STAT_VALUE_MAX = "value_max"
|
||||
STAT_VALUE_MIN = "value_min"
|
||||
STAT_VARIANCE = "variance"
|
||||
|
||||
CONF_PERIOD = "period"
|
||||
|
||||
# Statistics supported by a sensor source (numeric)
|
||||
STATS_NUMERIC_SUPPORT = {
|
||||
STAT_AVERAGE_LINEAR,
|
||||
@@ -117,6 +139,23 @@ STATS_NUMERIC_SUPPORT = {
|
||||
STAT_VARIANCE,
|
||||
}
|
||||
|
||||
# Statistics supported by a long term statistic source (numeric)
|
||||
STATS_NUMERIC_SUPPORT_LTS = {
|
||||
STAT_AVERAGE_STEP_LTS,
|
||||
STAT_SUM_DIFFERENCES_LTS,
|
||||
STAT_VALUE_MAX_LTS,
|
||||
STAT_VALUE_MIN_LTS,
|
||||
}
|
||||
|
||||
STATS_LTS_TO_RECORDER_CHARACTERISTIC: dict[
|
||||
str, Literal["max", "mean", "min", "change"]
|
||||
] = {
|
||||
STAT_AVERAGE_STEP_LTS: "mean",
|
||||
STAT_SUM_DIFFERENCES_LTS: "change",
|
||||
STAT_VALUE_MAX_LTS: "max",
|
||||
STAT_VALUE_MIN_LTS: "min",
|
||||
}
|
||||
|
||||
# Statistics supported by a binary_sensor source
|
||||
STATS_BINARY_SUPPORT = {
|
||||
STAT_AVERAGE_STEP,
|
||||
@@ -172,13 +211,12 @@ STAT_BINARY_PERCENTAGE = {
|
||||
STAT_MEAN,
|
||||
}
|
||||
|
||||
CONF_STATE_CHARACTERISTIC = "state_characteristic"
|
||||
CONF_SAMPLES_MAX_BUFFER_SIZE = "sampling_size"
|
||||
CONF_MAX_AGE = "max_age"
|
||||
CONF_PRECISION = "precision"
|
||||
CONF_PERCENTILE = "percentile"
|
||||
|
||||
DEFAULT_NAME = "Statistical characteristic"
|
||||
DEFAULT_PERCENTILE = 50
|
||||
DEFAULT_PRECISION = 2
|
||||
ICON = "mdi:calculator"
|
||||
|
||||
@@ -222,7 +260,7 @@ _PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend(
|
||||
),
|
||||
vol.Optional(CONF_MAX_AGE): cv.time_period,
|
||||
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int),
|
||||
vol.Optional(CONF_PERCENTILE, default=50): vol.All(
|
||||
vol.Optional(CONF_PERCENTILE, default=DEFAULT_PERCENTILE): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=99)
|
||||
),
|
||||
}
|
||||
@@ -234,6 +272,36 @@ PLATFORM_SCHEMA = vol.All(
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize min/max/mean config entry."""
|
||||
registry = er.async_get(hass)
|
||||
source_entity_id = er.async_validate_entity_id(
|
||||
registry, config_entry.options[CONF_ENTITY_ID]
|
||||
)
|
||||
state_characteristic = config_entry.options[CONF_STATE_CHARACTERISTIC]
|
||||
|
||||
entity: SensorEntity | None = None
|
||||
|
||||
if state_characteristic in STATS_NUMERIC_SUPPORT_LTS:
|
||||
entity = LTSStatisticsSensor(
|
||||
source_entity_id=source_entity_id,
|
||||
name=config_entry.title,
|
||||
unique_id=config_entry.entry_id,
|
||||
state_characteristic=state_characteristic,
|
||||
precision=DEFAULT_PRECISION,
|
||||
period=PERIOD_SCHEMA(config_entry.options["period"]),
|
||||
)
|
||||
|
||||
if not entity:
|
||||
return
|
||||
|
||||
async_add_entities([entity], update_before_add=True)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
@@ -749,3 +817,101 @@ class StatisticsSensor(SensorEntity):
|
||||
if len(self.states) > 0:
|
||||
return 100.0 / len(self.states) * self.states.count(True)
|
||||
return None
|
||||
|
||||
|
||||
class LTSStatisticsSensor(SensorEntity):
|
||||
"""Representation of a Statistics sensor sourcing data from the LTS tables."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source_entity_id: str,
|
||||
name: str,
|
||||
unique_id: str | None,
|
||||
state_characteristic: str,
|
||||
precision: int,
|
||||
period: StatisticPeriod,
|
||||
) -> None:
|
||||
"""Initialize the Statistics sensor."""
|
||||
self._attr_icon: str = ICON
|
||||
self._attr_name: str = name
|
||||
self._attr_unique_id: str | None = unique_id
|
||||
self._source_entity_id: str = source_entity_id
|
||||
self._recorder_characteristic: Literal[
|
||||
"max", "mean", "min", "change"
|
||||
] = STATS_LTS_TO_RECORDER_CHARACTERISTIC[state_characteristic]
|
||||
self._precision: int = precision
|
||||
self._value: StateType = None
|
||||
self._period = period
|
||||
|
||||
self._update_listener: CALLBACK_TYPE | None = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
|
||||
@callback
|
||||
def async_stats_updated_listener(_event: Event | None) -> None:
|
||||
"""Handle recorder LTS updated."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_stats_sensor_startup(_: HomeAssistant) -> None:
|
||||
"""Add listener and get recorded state."""
|
||||
_LOGGER.debug("Startup for %s", self.entity_id)
|
||||
|
||||
self.async_on_remove(
|
||||
self.hass.bus.async_listen(
|
||||
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
|
||||
async_stats_updated_listener,
|
||||
)
|
||||
)
|
||||
|
||||
self.async_on_remove(async_at_start(self.hass, async_stats_sensor_startup))
|
||||
|
||||
@property
|
||||
def device_class(self) -> SensorDeviceClass | None:
|
||||
"""Return the class of this entity."""
|
||||
_state = self.hass.states.get(self._source_entity_id)
|
||||
return None if _state is None else _state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
|
||||
@property
|
||||
def state_class(self) -> Literal[SensorStateClass.MEASUREMENT]:
|
||||
"""Return the state class of this entity."""
|
||||
return SensorStateClass.MEASUREMENT
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data and updates the states."""
|
||||
_LOGGER.debug("%s: updating statistics", self.entity_id)
|
||||
await get_instance(self.hass).async_add_executor_job(
|
||||
self._update_long_term_stats_from_database
|
||||
)
|
||||
|
||||
def _update_long_term_stats_from_database(self) -> None:
|
||||
"""Update the long term statistics from the database."""
|
||||
|
||||
result = get_metadata(
|
||||
self.hass,
|
||||
statistic_ids=[self._source_entity_id],
|
||||
).get(self._source_entity_id)
|
||||
if result:
|
||||
self._attr_native_unit_of_measurement = result[1].get("unit_of_measurement")
|
||||
else:
|
||||
self._attr_native_unit_of_measurement = None
|
||||
|
||||
start_time, end_time = resolve_period(self._period)
|
||||
stat = statistic_during_period(
|
||||
self.hass,
|
||||
start_time,
|
||||
end_time,
|
||||
self._source_entity_id,
|
||||
{self._recorder_characteristic},
|
||||
None,
|
||||
)
|
||||
value = stat.get(self._recorder_characteristic)
|
||||
with contextlib.suppress(TypeError):
|
||||
value = round(cast(float, value), self._precision)
|
||||
if self._precision == 0:
|
||||
value = int(value)
|
||||
|
||||
self._attr_native_value = value
|
||||
self._attr_available = self._attr_native_value is not None
|
||||
|
||||
@@ -9,6 +9,7 @@ FLOWS = {
|
||||
"group",
|
||||
"integration",
|
||||
"min_max",
|
||||
"statistics",
|
||||
"switch_as_x",
|
||||
"threshold",
|
||||
"tod",
|
||||
|
||||
@@ -5099,12 +5099,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",
|
||||
@@ -6352,6 +6346,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,
|
||||
|
||||
45
tests/components/statistics/common.py
Normal file
45
tests/components/statistics/common.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Test helpers for the statistics integration."""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.components import recorder
|
||||
from homeassistant.components.recorder.db_schema import Statistics
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.recorder.common import async_wait_recording_done
|
||||
|
||||
|
||||
async def generate_statistics(
|
||||
hass: HomeAssistant,
|
||||
entity_id: str,
|
||||
start: datetime,
|
||||
count: int,
|
||||
start_value: float = 0.0,
|
||||
) -> None:
|
||||
"""Generate LTS data."""
|
||||
|
||||
imported_stats = [
|
||||
{
|
||||
"start": (start + timedelta(hours=i)),
|
||||
"max": start_value + i * 2,
|
||||
"mean": start_value + i,
|
||||
"min": -(start_value + count * 2) + i * 2,
|
||||
"sum": start_value + i,
|
||||
}
|
||||
for i in range(0, count)
|
||||
]
|
||||
|
||||
imported_metadata = {
|
||||
"has_mean": True,
|
||||
"has_sum": False,
|
||||
"name": None,
|
||||
"source": "recorder",
|
||||
"statistic_id": entity_id,
|
||||
"unit_of_measurement": "°C",
|
||||
}
|
||||
|
||||
recorder.get_instance(hass).async_import_statistics(
|
||||
imported_metadata,
|
||||
imported_stats,
|
||||
Statistics,
|
||||
)
|
||||
await async_wait_recording_done(hass)
|
||||
294
tests/components/statistics/test_config_flow.py
Normal file
294
tests/components/statistics/test_config_flow.py
Normal file
@@ -0,0 +1,294 @@
|
||||
"""Test the Statistics config flow."""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.statistics.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .common import generate_statistics
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"period_type, period_input, period_data",
|
||||
(
|
||||
(
|
||||
"calendar",
|
||||
{
|
||||
"calendar_offset": 2,
|
||||
"calendar_period": "day",
|
||||
},
|
||||
{"offset": 2, "period": "day"},
|
||||
),
|
||||
(
|
||||
"fixed_period",
|
||||
{
|
||||
"fixed_period_end_time": "2022-03-24 00:00",
|
||||
"fixed_period_start_time": "2022-03-24 00:00",
|
||||
},
|
||||
{"end_time": "2022-03-24 00:00", "start_time": "2022-03-24 00:00"},
|
||||
),
|
||||
(
|
||||
"rolling_window",
|
||||
{
|
||||
"rolling_window_duration": {"days": 365},
|
||||
"rolling_window_offset": {"days": -365},
|
||||
},
|
||||
{"duration": {"days": 365}, "offset": {"days": -365}},
|
||||
),
|
||||
),
|
||||
)
|
||||
async def test_config_flow(
|
||||
hass: HomeAssistant, period_type, period_input, period_data
|
||||
) -> None:
|
||||
"""Test the config flow."""
|
||||
input_sensor = "sensor.input_one"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"name": "My statistics",
|
||||
"entity_id": input_sensor,
|
||||
"state_characteristic": "value_max_lts",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": period_type},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == period_type
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.statistics.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
period_input,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "My statistics"
|
||||
assert result["data"] == {}
|
||||
assert result["options"] == {
|
||||
"entity_id": input_sensor,
|
||||
"name": "My statistics",
|
||||
"period": {period_type: period_data},
|
||||
"precision": 2.0,
|
||||
"state_characteristic": "value_max_lts",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
assert config_entry.data == {}
|
||||
assert config_entry.options == {
|
||||
"entity_id": input_sensor,
|
||||
"name": "My statistics",
|
||||
"period": {period_type: period_data},
|
||||
"precision": 2.0,
|
||||
"state_characteristic": "value_max_lts",
|
||||
}
|
||||
assert config_entry.title == "My statistics"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"period_type, period_definition",
|
||||
(
|
||||
(
|
||||
"calendar",
|
||||
{
|
||||
"period": "day",
|
||||
"offset": 2,
|
||||
},
|
||||
),
|
||||
(
|
||||
"fixed_period",
|
||||
{
|
||||
"start_time": "2022-03-24 00:00",
|
||||
"end_time": "2022-03-24 00:00",
|
||||
},
|
||||
),
|
||||
(
|
||||
"rolling_window",
|
||||
{
|
||||
"duration": {"days": 365},
|
||||
"offset": {"days": -365},
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
async def test_config_flow_import(
|
||||
hass: HomeAssistant, period_type, period_definition
|
||||
) -> None:
|
||||
"""Test the config flow."""
|
||||
input_sensor = "sensor.input_one"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.statistics.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={
|
||||
"entity_id": input_sensor,
|
||||
"name": "My statistics",
|
||||
"period": {period_type: period_definition},
|
||||
"precision": 2.0,
|
||||
"state_characteristic": "value_max_lts",
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "My statistics"
|
||||
assert result["data"] == {}
|
||||
assert result["options"] == {
|
||||
"entity_id": input_sensor,
|
||||
"name": "My statistics",
|
||||
"period": {period_type: period_definition},
|
||||
"precision": 2.0,
|
||||
"state_characteristic": "value_max_lts",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
assert config_entry.data == {}
|
||||
assert config_entry.options == {
|
||||
"entity_id": input_sensor,
|
||||
"name": "My statistics",
|
||||
"period": {period_type: period_definition},
|
||||
"precision": 2.0,
|
||||
"state_characteristic": "value_max_lts",
|
||||
}
|
||||
assert config_entry.title == "My statistics"
|
||||
|
||||
|
||||
def get_suggested(schema, key):
|
||||
"""Get suggested value for key in voluptuous schema."""
|
||||
for k in schema.keys():
|
||||
if k == key:
|
||||
if k.description is None or "suggested_value" not in k.description:
|
||||
return None
|
||||
return k.description["suggested_value"]
|
||||
# Wanted key absent from schema
|
||||
raise Exception
|
||||
|
||||
|
||||
@freeze_time(datetime(2022, 10, 21, 7, 25, tzinfo=timezone.utc))
|
||||
async def test_options(recorder_mock, hass: HomeAssistant) -> None:
|
||||
"""Test reconfiguring."""
|
||||
|
||||
now = dt_util.utcnow()
|
||||
|
||||
zero = now
|
||||
start = zero.replace(minute=0, second=0, microsecond=0) + timedelta(days=-2)
|
||||
|
||||
await generate_statistics(hass, "sensor.input_one", start, 6)
|
||||
await generate_statistics(hass, "sensor.input_two", start, 6)
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={
|
||||
"entity_id": "sensor.input_one",
|
||||
"name": "My statistics",
|
||||
"period": {"calendar": {"offset": -2, "period": "day"}},
|
||||
"precision": 2.0,
|
||||
"state_characteristic": "value_max_lts",
|
||||
},
|
||||
title="My statistics",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check the state of the entity is reflecting the initial settings
|
||||
state = hass.states.get("sensor.my_statistics")
|
||||
assert state.state == "10.0"
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
schema = result["data_schema"].schema
|
||||
assert get_suggested(schema, "entity_id") == "sensor.input_one"
|
||||
assert get_suggested(schema, "precision") == 2.0
|
||||
assert get_suggested(schema, "state_characteristic") == "value_max_lts"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"entity_id": "sensor.input_two",
|
||||
"precision": 1,
|
||||
"state_characteristic": "value_min_lts",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "rolling_window"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "rolling_window"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"rolling_window_duration": {"days": 2},
|
||||
"rolling_window_offset": {"hours": -1},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"entity_id": "sensor.input_two",
|
||||
"name": "My statistics",
|
||||
"period": {
|
||||
"rolling_window": {"duration": {"days": 2}, "offset": {"hours": -1}}
|
||||
},
|
||||
"precision": 1.0,
|
||||
"state_characteristic": "value_min_lts",
|
||||
}
|
||||
assert config_entry.data == {}
|
||||
assert config_entry.options == {
|
||||
"entity_id": "sensor.input_two",
|
||||
"name": "My statistics",
|
||||
"period": {
|
||||
"rolling_window": {"duration": {"days": 2}, "offset": {"hours": -1}}
|
||||
},
|
||||
"precision": 1.0,
|
||||
"state_characteristic": "value_min_lts",
|
||||
}
|
||||
assert config_entry.title == "My statistics"
|
||||
|
||||
# Check config entry is reloaded with new options
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check the entity was updated, no new entity was created
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
# Check the state of the entity has changed as expected
|
||||
state = hass.states.get("sensor.my_statistics")
|
||||
assert state.state == "-12.0"
|
||||
@@ -13,7 +13,7 @@ from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN
|
||||
from homeassistant.components.statistics.const import DOMAIN as STATISTICS_DOMAIN
|
||||
from homeassistant.components.statistics.sensor import StatisticsSensor
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
|
||||
Reference in New Issue
Block a user