mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
Add config flow to trend (#99761)
* Add config flow to trend * Remove device_class from options flow * Add min_samples and import step to config flow * Fix import * Fixing tests and some cleanup * remove unneeded usefixtures * Apply code review suggestions * Re-add YAML support * Re-add reload service * Fix import * Apply code review suggestions * Add test coverage for yaml setup --------- Co-authored-by: G Johansson <goran.johansson@shiftit.se>
This commit is contained in:
parent
4decc2bbfb
commit
e04fda3fad
@ -1,5 +1,27 @@
|
||||
"""A sensor that monitors trends in other components."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Trend from a config entry."""
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(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:
|
||||
"""Handle an Trend options update."""
|
||||
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)
|
||||
|
@ -17,6 +17,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
@ -54,6 +55,10 @@ from .const import (
|
||||
CONF_MIN_GRADIENT,
|
||||
CONF_MIN_SAMPLES,
|
||||
CONF_SAMPLE_DURATION,
|
||||
DEFAULT_MAX_SAMPLES,
|
||||
DEFAULT_MIN_GRADIENT,
|
||||
DEFAULT_MIN_SAMPLES,
|
||||
DEFAULT_SAMPLE_DURATION,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
@ -101,40 +106,52 @@ async def async_setup_platform(
|
||||
"""Set up the trend sensors."""
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
|
||||
sensors = []
|
||||
|
||||
for device_id, device_config in config[CONF_SENSORS].items():
|
||||
entity_id = device_config[ATTR_ENTITY_ID]
|
||||
attribute = device_config.get(CONF_ATTRIBUTE)
|
||||
device_class = device_config.get(CONF_DEVICE_CLASS)
|
||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device_id)
|
||||
invert = device_config[CONF_INVERT]
|
||||
max_samples = device_config[CONF_MAX_SAMPLES]
|
||||
min_gradient = device_config[CONF_MIN_GRADIENT]
|
||||
sample_duration = device_config[CONF_SAMPLE_DURATION]
|
||||
min_samples = device_config[CONF_MIN_SAMPLES]
|
||||
|
||||
sensors.append(
|
||||
entities = []
|
||||
for sensor_name, sensor_config in config[CONF_SENSORS].items():
|
||||
entities.append(
|
||||
SensorTrend(
|
||||
hass,
|
||||
device_id,
|
||||
friendly_name,
|
||||
entity_id,
|
||||
attribute,
|
||||
device_class,
|
||||
invert,
|
||||
max_samples,
|
||||
min_gradient,
|
||||
sample_duration,
|
||||
min_samples,
|
||||
name=sensor_config.get(CONF_FRIENDLY_NAME, sensor_name),
|
||||
entity_id=sensor_config[CONF_ENTITY_ID],
|
||||
attribute=sensor_config.get(CONF_ATTRIBUTE),
|
||||
invert=sensor_config[CONF_INVERT],
|
||||
sample_duration=sensor_config[CONF_SAMPLE_DURATION],
|
||||
min_gradient=sensor_config[CONF_MIN_GRADIENT],
|
||||
min_samples=sensor_config[CONF_MIN_SAMPLES],
|
||||
max_samples=sensor_config[CONF_MAX_SAMPLES],
|
||||
device_class=sensor_config.get(CONF_DEVICE_CLASS),
|
||||
sensor_entity_id=generate_entity_id(
|
||||
ENTITY_ID_FORMAT, sensor_name, hass=hass
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if not sensors:
|
||||
_LOGGER.error("No sensors added")
|
||||
return
|
||||
async_add_entities(entities)
|
||||
|
||||
async_add_entities(sensors)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up trend sensor from config entry."""
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
SensorTrend(
|
||||
name=entry.title,
|
||||
entity_id=entry.options[CONF_ENTITY_ID],
|
||||
attribute=entry.options.get(CONF_ATTRIBUTE),
|
||||
invert=entry.options[CONF_INVERT],
|
||||
sample_duration=entry.options.get(
|
||||
CONF_SAMPLE_DURATION, DEFAULT_SAMPLE_DURATION
|
||||
),
|
||||
min_gradient=entry.options.get(CONF_MIN_GRADIENT, DEFAULT_MIN_GRADIENT),
|
||||
min_samples=entry.options.get(CONF_MIN_SAMPLES, DEFAULT_MIN_SAMPLES),
|
||||
max_samples=entry.options.get(CONF_MAX_SAMPLES, DEFAULT_MAX_SAMPLES),
|
||||
unique_id=entry.entry_id,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SensorTrend(BinarySensorEntity, RestoreEntity):
|
||||
@ -146,30 +163,33 @@ class SensorTrend(BinarySensorEntity, RestoreEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
device_id: str,
|
||||
friendly_name: str,
|
||||
name: str,
|
||||
entity_id: str,
|
||||
attribute: str,
|
||||
device_class: BinarySensorDeviceClass,
|
||||
attribute: str | None,
|
||||
invert: bool,
|
||||
max_samples: int,
|
||||
min_gradient: float,
|
||||
sample_duration: int,
|
||||
min_gradient: float,
|
||||
min_samples: int,
|
||||
max_samples: int,
|
||||
unique_id: str | None = None,
|
||||
device_class: BinarySensorDeviceClass | None = None,
|
||||
sensor_entity_id: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._hass = hass
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||
self._attr_name = friendly_name
|
||||
self._attr_device_class = device_class
|
||||
self._entity_id = entity_id
|
||||
self._attribute = attribute
|
||||
self._invert = invert
|
||||
self._sample_duration = sample_duration
|
||||
self._min_gradient = min_gradient
|
||||
self._min_samples = min_samples
|
||||
self.samples: deque = deque(maxlen=max_samples)
|
||||
self.samples: deque = deque(maxlen=int(max_samples))
|
||||
|
||||
self._attr_name = name
|
||||
self._attr_device_class = device_class
|
||||
self._attr_unique_id = unique_id
|
||||
|
||||
if sensor_entity_id:
|
||||
self.entity_id = sensor_entity_id
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
|
111
homeassistant/components/trend/config_flow.py
Normal file
111
homeassistant/components/trend/config_flow.py
Normal file
@ -0,0 +1,111 @@
|
||||
"""Config flow for Trend integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_NAME, UnitOfTime
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaCommonFlowHandler,
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_INVERT,
|
||||
CONF_MAX_SAMPLES,
|
||||
CONF_MIN_GRADIENT,
|
||||
CONF_MIN_SAMPLES,
|
||||
CONF_SAMPLE_DURATION,
|
||||
DEFAULT_MAX_SAMPLES,
|
||||
DEFAULT_MIN_GRADIENT,
|
||||
DEFAULT_MIN_SAMPLES,
|
||||
DEFAULT_SAMPLE_DURATION,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
async def get_base_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
|
||||
"""Get base options schema."""
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ATTRIBUTE): selector.AttributeSelector(
|
||||
selector.AttributeSelectorConfig(
|
||||
entity_id=handler.options[CONF_ENTITY_ID]
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_INVERT, default=False): selector.BooleanSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def get_extended_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
|
||||
"""Get extended options schema."""
|
||||
return (await get_base_options_schema(handler)).extend(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_MAX_SAMPLES, default=DEFAULT_MAX_SAMPLES
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=2,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
),
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_MIN_SAMPLES, default=DEFAULT_MIN_SAMPLES
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=2,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
),
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_MIN_GRADIENT, default=DEFAULT_MIN_GRADIENT
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0,
|
||||
step="any",
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
),
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_SAMPLE_DURATION, default=DEFAULT_SAMPLE_DURATION
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
unit_of_measurement=UnitOfTime.SECONDS,
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): selector.TextSelector(),
|
||||
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=SENSOR_DOMAIN, multiple=False),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config or options flow for Trend."""
|
||||
|
||||
config_flow = {
|
||||
"user": SchemaFlowFormStep(schema=CONFIG_SCHEMA, next_step="settings"),
|
||||
"settings": SchemaFlowFormStep(get_base_options_schema),
|
||||
}
|
||||
options_flow = {
|
||||
"init": SchemaFlowFormStep(get_extended_options_schema),
|
||||
}
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
return cast(str, options[CONF_NAME])
|
@ -13,3 +13,8 @@ CONF_MAX_SAMPLES = "max_samples"
|
||||
CONF_MIN_GRADIENT = "min_gradient"
|
||||
CONF_SAMPLE_DURATION = "sample_duration"
|
||||
CONF_MIN_SAMPLES = "min_samples"
|
||||
|
||||
DEFAULT_MAX_SAMPLES = 2
|
||||
DEFAULT_MIN_SAMPLES = 2
|
||||
DEFAULT_MIN_GRADIENT = 0.0
|
||||
DEFAULT_SAMPLE_DURATION = 0
|
||||
|
@ -2,7 +2,9 @@
|
||||
"domain": "trend",
|
||||
"name": "Trend",
|
||||
"codeowners": ["@jpbede"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/trend",
|
||||
"integration_type": "helper",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["numpy==1.26.0"]
|
||||
|
@ -4,5 +4,43 @@
|
||||
"name": "[%key:common::action::reload%]",
|
||||
"description": "Reloads trend sensors from the YAML-configuration."
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Trend helper",
|
||||
"description": "The trend helper allows you to create a sensor which show the trend of a numeric state or a state attribute from another entity.",
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"entity_id": "Entity that this sensor tracks"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"data": {
|
||||
"attribute": "Attribute of entity that this sensor tracks",
|
||||
"invert": "Invert the result"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"attribute": "[%key:component::trend::config::step::settings::data::attribute%]",
|
||||
"invert": "[%key:component::trend::config::step::settings::data::invert%]",
|
||||
"max_samples": "Maximum number of stored samples",
|
||||
"min_samples": "Minimum number of stored samples",
|
||||
"min_gradient": "Minimum rate at which the value must be changing",
|
||||
"sample_duration": "Duration in seconds to store samples for"
|
||||
},
|
||||
"data_description": {
|
||||
"max_samples": "The maximum number of samples to store. If the number of samples exceeds this value, the oldest samples will be discarded.",
|
||||
"min_samples": "The minimum number of samples that must be collected before the gradient can be calculated.",
|
||||
"min_gradient": "The minimum rate at which the observed value must be changing for this sensor to switch on. The gradient is measured in sensor units per second.",
|
||||
"sample_duration": "The duration in seconds to store samples for. Samples older than this value will be discarded."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ FLOWS = {
|
||||
"template",
|
||||
"threshold",
|
||||
"tod",
|
||||
"trend",
|
||||
"utility_meter",
|
||||
],
|
||||
"integration": [
|
||||
|
@ -6113,12 +6113,6 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"trend": {
|
||||
"name": "Trend",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "calculated"
|
||||
},
|
||||
"tuya": {
|
||||
"name": "Tuya",
|
||||
"integration_type": "hub",
|
||||
@ -6944,6 +6938,12 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "calculated"
|
||||
},
|
||||
"trend": {
|
||||
"name": "Trend",
|
||||
"integration_type": "helper",
|
||||
"config_flow": true,
|
||||
"iot_class": "calculated"
|
||||
},
|
||||
"utility_meter": {
|
||||
"integration_type": "helper",
|
||||
"config_flow": true,
|
||||
|
51
tests/components/trend/conftest.py
Normal file
51
tests/components/trend/conftest.py
Normal file
@ -0,0 +1,51 @@
|
||||
"""Fixtures for the trend component tests."""
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.trend.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
ComponentSetup = Callable[[dict[str, Any]], Awaitable[None]]
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry")
|
||||
async def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Return a MockConfigEntry for testing."""
|
||||
return MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={
|
||||
"name": "My trend",
|
||||
"entity_id": "sensor.cpu_temp",
|
||||
"invert": False,
|
||||
"max_samples": 2.0,
|
||||
"min_gradient": 0.0,
|
||||
"sample_duration": 0.0,
|
||||
},
|
||||
title="My trend",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="setup_component")
|
||||
async def mock_setup_component(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> ComponentSetup:
|
||||
"""Set up the trend component."""
|
||||
|
||||
async def _setup_func(component_params: dict[str, Any]) -> None:
|
||||
config_entry.title = "test_trend_sensor"
|
||||
config_entry.options = {
|
||||
**config_entry.options,
|
||||
**component_params,
|
||||
"name": "test_trend_sensor",
|
||||
"entity_id": "sensor.test_state",
|
||||
}
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return _setup_func
|
@ -2,22 +2,22 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant import config as hass_config, setup
|
||||
from homeassistant.components.trend.const import DOMAIN
|
||||
from homeassistant.const import SERVICE_RELOAD, STATE_OFF, STATE_ON, STATE_UNKNOWN
|
||||
from homeassistant import setup
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import assert_setup_component, get_fixture_path, mock_restore_cache
|
||||
from .conftest import ComponentSetup
|
||||
|
||||
from tests.common import MockConfigEntry, assert_setup_component, mock_restore_cache
|
||||
|
||||
|
||||
async def _setup_component(hass: HomeAssistant, params: dict[str, Any]) -> None:
|
||||
"""Set up the trend component."""
|
||||
async def _setup_legacy_component(hass: HomeAssistant, params: dict[str, Any]) -> None:
|
||||
"""Set up the trend component the legacy way."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"binary_sensor",
|
||||
@ -43,17 +43,54 @@ async def _setup_component(hass: HomeAssistant, params: dict[str, Any]) -> None:
|
||||
],
|
||||
ids=["up", "down", "up inverted", "down inverted"],
|
||||
)
|
||||
async def test_basic_trend(
|
||||
async def test_basic_trend_setup_from_yaml(
|
||||
hass: HomeAssistant,
|
||||
states: list[str],
|
||||
inverted: bool,
|
||||
expected_state: str,
|
||||
):
|
||||
) -> None:
|
||||
"""Test trend with a basic setup."""
|
||||
await _setup_component(
|
||||
await _setup_legacy_component(
|
||||
hass,
|
||||
{
|
||||
"entity_id": "sensor.test_state",
|
||||
"friendly_name": "Test state",
|
||||
"entity_id": "sensor.cpu_temp",
|
||||
"invert": inverted,
|
||||
"max_samples": 2.0,
|
||||
"min_gradient": 0.0,
|
||||
"sample_duration": 0.0,
|
||||
},
|
||||
)
|
||||
|
||||
for state in states:
|
||||
hass.states.async_set("sensor.cpu_temp", state)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (sensor_state := hass.states.get("binary_sensor.test_trend_sensor"))
|
||||
assert sensor_state.state == expected_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("states", "inverted", "expected_state"),
|
||||
[
|
||||
(["1", "2"], False, STATE_ON),
|
||||
(["2", "1"], False, STATE_OFF),
|
||||
(["1", "2"], True, STATE_OFF),
|
||||
(["2", "1"], True, STATE_ON),
|
||||
],
|
||||
ids=["up", "down", "up inverted", "down inverted"],
|
||||
)
|
||||
async def test_basic_trend(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
setup_component: ComponentSetup,
|
||||
states: list[str],
|
||||
inverted: bool,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test trend with a basic setup."""
|
||||
await setup_component(
|
||||
{
|
||||
"invert": inverted,
|
||||
},
|
||||
)
|
||||
@ -89,16 +126,16 @@ async def test_basic_trend(
|
||||
)
|
||||
async def test_using_trendline(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
setup_component: ComponentSetup,
|
||||
state_series: list[list[str]],
|
||||
inverted: bool,
|
||||
expected_states: list[str],
|
||||
):
|
||||
) -> None:
|
||||
"""Test uptrend using multiple samples and trendline calculation."""
|
||||
await _setup_component(
|
||||
hass,
|
||||
await setup_component(
|
||||
{
|
||||
"entity_id": "sensor.test_state",
|
||||
"sample_duration": 10000,
|
||||
"min_gradient": 1,
|
||||
"max_samples": 25,
|
||||
@ -127,12 +164,13 @@ async def test_using_trendline(
|
||||
)
|
||||
async def test_attribute_trend(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
setup_component: ComponentSetup,
|
||||
attr_values: list[str],
|
||||
expected_state: str,
|
||||
):
|
||||
) -> None:
|
||||
"""Test attribute uptrend."""
|
||||
await _setup_component(
|
||||
hass,
|
||||
await setup_component(
|
||||
{
|
||||
"entity_id": "sensor.test_state",
|
||||
"attribute": "attr",
|
||||
@ -147,12 +185,12 @@ async def test_attribute_trend(
|
||||
assert sensor_state.state == expected_state
|
||||
|
||||
|
||||
async def test_max_samples(hass: HomeAssistant):
|
||||
async def test_max_samples(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, setup_component: ComponentSetup
|
||||
) -> None:
|
||||
"""Test that sample count is limited correctly."""
|
||||
await _setup_component(
|
||||
hass,
|
||||
await setup_component(
|
||||
{
|
||||
"entity_id": "sensor.test_state",
|
||||
"max_samples": 3,
|
||||
"min_gradient": -1,
|
||||
},
|
||||
@ -167,39 +205,39 @@ async def test_max_samples(hass: HomeAssistant):
|
||||
assert state.attributes["sample_count"] == 3
|
||||
|
||||
|
||||
async def test_non_numeric(hass: HomeAssistant):
|
||||
async def test_non_numeric(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, setup_component: ComponentSetup
|
||||
) -> None:
|
||||
"""Test for non-numeric sensor."""
|
||||
await _setup_component(hass, {"entity_id": "sensor.test_state"})
|
||||
await setup_component({"entity_id": "sensor.test_state"})
|
||||
|
||||
hass.states.async_set("sensor.test_state", "Non")
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_state", "Numeric")
|
||||
await hass.async_block_till_done()
|
||||
for val in ["Non", "Numeric"]:
|
||||
hass.states.async_set("sensor.test_state", val)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get("binary_sensor.test_trend_sensor"))
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_missing_attribute(hass: HomeAssistant):
|
||||
async def test_missing_attribute(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, setup_component: ComponentSetup
|
||||
) -> None:
|
||||
"""Test for missing attribute."""
|
||||
await _setup_component(
|
||||
hass,
|
||||
await setup_component(
|
||||
{
|
||||
"entity_id": "sensor.test_state",
|
||||
"attribute": "missing",
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("sensor.test_state", "State", {"attr": "2"})
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_state", "State", {"attr": "1"})
|
||||
await hass.async_block_till_done()
|
||||
for val in [1, 2]:
|
||||
hass.states.async_set("sensor.test_state", "State", {"attr": val})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get("binary_sensor.test_trend_sensor"))
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_invalid_name_does_not_create(hass: HomeAssistant):
|
||||
async def test_invalid_name_does_not_create(hass: HomeAssistant) -> None:
|
||||
"""Test for invalid name."""
|
||||
with assert_setup_component(0):
|
||||
assert await setup.async_setup_component(
|
||||
@ -217,7 +255,7 @@ async def test_invalid_name_does_not_create(hass: HomeAssistant):
|
||||
assert hass.states.async_all("binary_sensor") == []
|
||||
|
||||
|
||||
async def test_invalid_sensor_does_not_create(hass: HomeAssistant):
|
||||
async def test_invalid_sensor_does_not_create(hass: HomeAssistant) -> None:
|
||||
"""Test invalid sensor."""
|
||||
with assert_setup_component(0):
|
||||
assert await setup.async_setup_component(
|
||||
@ -235,7 +273,7 @@ async def test_invalid_sensor_does_not_create(hass: HomeAssistant):
|
||||
assert hass.states.async_all("binary_sensor") == []
|
||||
|
||||
|
||||
async def test_no_sensors_does_not_create(hass: HomeAssistant):
|
||||
async def test_no_sensors_does_not_create(hass: HomeAssistant) -> None:
|
||||
"""Test no sensors."""
|
||||
with assert_setup_component(0):
|
||||
assert await setup.async_setup_component(
|
||||
@ -244,59 +282,23 @@ async def test_no_sensors_does_not_create(hass: HomeAssistant):
|
||||
assert hass.states.async_all("binary_sensor") == []
|
||||
|
||||
|
||||
async def test_reload(hass: HomeAssistant) -> None:
|
||||
"""Verify we can reload trend sensors."""
|
||||
hass.states.async_set("sensor.test_state", 1234)
|
||||
|
||||
await setup.async_setup_component(
|
||||
hass,
|
||||
"binary_sensor",
|
||||
{
|
||||
"binary_sensor": {
|
||||
"platform": "trend",
|
||||
"sensors": {"test_trend_sensor": {"entity_id": "sensor.test_state"}},
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 2
|
||||
|
||||
assert hass.states.get("binary_sensor.test_trend_sensor")
|
||||
|
||||
yaml_path = get_fixture_path("configuration.yaml", "trend")
|
||||
with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_RELOAD,
|
||||
{},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 2
|
||||
|
||||
assert hass.states.get("binary_sensor.test_trend_sensor") is None
|
||||
assert hass.states.get("binary_sensor.second_test_trend_sensor")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("saved_state", "restored_state"),
|
||||
[("on", "on"), ("off", "off"), ("unknown", "unknown")],
|
||||
)
|
||||
async def test_restore_state(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
setup_component: ComponentSetup,
|
||||
saved_state: str,
|
||||
restored_state: str,
|
||||
) -> None:
|
||||
"""Test we restore the trend state."""
|
||||
mock_restore_cache(hass, (State("binary_sensor.test_trend_sensor", saved_state),))
|
||||
|
||||
await _setup_component(
|
||||
hass,
|
||||
await setup_component(
|
||||
{
|
||||
"entity_id": "sensor.test_state",
|
||||
"sample_duration": 10000,
|
||||
"min_gradient": 1,
|
||||
"max_samples": 25,
|
||||
@ -332,7 +334,7 @@ async def test_invalid_min_sample(
|
||||
) -> None:
|
||||
"""Test if error is logged when min_sample is larger than max_samples."""
|
||||
with caplog.at_level(logging.ERROR):
|
||||
await _setup_component(
|
||||
await _setup_legacy_component(
|
||||
hass,
|
||||
{
|
||||
"entity_id": "sensor.test_state",
|
||||
|
80
tests/components/trend/test_config_flow.py
Normal file
80
tests/components/trend/test_config_flow.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""Test the Trend config flow."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.trend import async_setup_entry
|
||||
from homeassistant.components.trend.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant) -> None:
|
||||
"""Test we get the form."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"name": "CPU Temperature rising", "entity_id": "sensor.cpu_temp"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
|
||||
# test step 2 of config flow: settings of trend sensor
|
||||
with patch(
|
||||
"homeassistant.components.trend.async_setup_entry", wraps=async_setup_entry
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"invert": False,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "CPU Temperature rising"
|
||||
assert result["data"] == {}
|
||||
assert result["options"] == {
|
||||
"entity_id": "sensor.cpu_temp",
|
||||
"invert": False,
|
||||
"name": "CPU Temperature rising",
|
||||
}
|
||||
|
||||
|
||||
async def test_options(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Test options flow."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"min_samples": 30,
|
||||
"max_samples": 50,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"min_samples": 30,
|
||||
"max_samples": 50,
|
||||
"entity_id": "sensor.cpu_temp",
|
||||
"invert": False,
|
||||
"min_gradient": 0.0,
|
||||
"name": "My trend",
|
||||
"sample_duration": 0.0,
|
||||
}
|
50
tests/components/trend/test_init.py
Normal file
50
tests/components/trend/test_init.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""Test the Trend integration."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.trend.conftest import ComponentSetup
|
||||
|
||||
|
||||
async def test_setup_and_remove_config_entry(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test setting up and removing a config entry."""
|
||||
registry = er.async_get(hass)
|
||||
trend_entity_id = "binary_sensor.my_trend"
|
||||
|
||||
# Set up the config entry
|
||||
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 entity is registered in the entity registry
|
||||
assert registry.async_get(trend_entity_id) is not None
|
||||
|
||||
# Remove the config entry
|
||||
assert await hass.config_entries.async_remove(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check the state and entity registry entry are removed
|
||||
assert hass.states.get(trend_entity_id) is None
|
||||
assert registry.async_get(trend_entity_id) is None
|
||||
|
||||
|
||||
async def test_reload_config_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
setup_component: ComponentSetup,
|
||||
) -> None:
|
||||
"""Test config entry reload."""
|
||||
await setup_component({})
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert hass.config_entries.async_update_entry(
|
||||
config_entry, data={**config_entry.data, "max_samples": 4.0}
|
||||
)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
assert config_entry.data == {**config_entry.data, "max_samples": 4.0}
|
Loading…
x
Reference in New Issue
Block a user