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:
Jan-Philipp Benecke 2023-12-27 14:46:57 +01:00 committed by GitHub
parent 4decc2bbfb
commit e04fda3fad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 508 additions and 126 deletions

View File

@ -1,5 +1,27 @@
"""A sensor that monitors trends in other components.""" """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.const import Platform
from homeassistant.core import HomeAssistant
PLATFORMS = [Platform.BINARY_SENSOR] 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)

View File

@ -17,6 +17,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME, ATTR_FRIENDLY_NAME,
@ -54,6 +55,10 @@ from .const import (
CONF_MIN_GRADIENT, CONF_MIN_GRADIENT,
CONF_MIN_SAMPLES, CONF_MIN_SAMPLES,
CONF_SAMPLE_DURATION, CONF_SAMPLE_DURATION,
DEFAULT_MAX_SAMPLES,
DEFAULT_MIN_GRADIENT,
DEFAULT_MIN_SAMPLES,
DEFAULT_SAMPLE_DURATION,
DOMAIN, DOMAIN,
) )
@ -101,40 +106,52 @@ async def async_setup_platform(
"""Set up the trend sensors.""" """Set up the trend sensors."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS) await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
sensors = [] entities = []
for sensor_name, sensor_config in config[CONF_SENSORS].items():
for device_id, device_config in config[CONF_SENSORS].items(): entities.append(
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(
SensorTrend( SensorTrend(
hass, name=sensor_config.get(CONF_FRIENDLY_NAME, sensor_name),
device_id, entity_id=sensor_config[CONF_ENTITY_ID],
friendly_name, attribute=sensor_config.get(CONF_ATTRIBUTE),
entity_id, invert=sensor_config[CONF_INVERT],
attribute, sample_duration=sensor_config[CONF_SAMPLE_DURATION],
device_class, min_gradient=sensor_config[CONF_MIN_GRADIENT],
invert, min_samples=sensor_config[CONF_MIN_SAMPLES],
max_samples, max_samples=sensor_config[CONF_MAX_SAMPLES],
min_gradient, device_class=sensor_config.get(CONF_DEVICE_CLASS),
sample_duration, sensor_entity_id=generate_entity_id(
min_samples, ENTITY_ID_FORMAT, sensor_name, hass=hass
),
) )
) )
if not sensors: async_add_entities(entities)
_LOGGER.error("No sensors added")
return
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): class SensorTrend(BinarySensorEntity, RestoreEntity):
@ -146,30 +163,33 @@ class SensorTrend(BinarySensorEntity, RestoreEntity):
def __init__( def __init__(
self, self,
hass: HomeAssistant, name: str,
device_id: str,
friendly_name: str,
entity_id: str, entity_id: str,
attribute: str, attribute: str | None,
device_class: BinarySensorDeviceClass,
invert: bool, invert: bool,
max_samples: int,
min_gradient: float,
sample_duration: int, sample_duration: int,
min_gradient: float,
min_samples: int, min_samples: int,
max_samples: int,
unique_id: str | None = None,
device_class: BinarySensorDeviceClass | None = None,
sensor_entity_id: str | None = None,
) -> None: ) -> None:
"""Initialize the sensor.""" """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._entity_id = entity_id
self._attribute = attribute self._attribute = attribute
self._invert = invert self._invert = invert
self._sample_duration = sample_duration self._sample_duration = sample_duration
self._min_gradient = min_gradient self._min_gradient = min_gradient
self._min_samples = min_samples 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 @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:

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

View File

@ -13,3 +13,8 @@ CONF_MAX_SAMPLES = "max_samples"
CONF_MIN_GRADIENT = "min_gradient" CONF_MIN_GRADIENT = "min_gradient"
CONF_SAMPLE_DURATION = "sample_duration" CONF_SAMPLE_DURATION = "sample_duration"
CONF_MIN_SAMPLES = "min_samples" CONF_MIN_SAMPLES = "min_samples"
DEFAULT_MAX_SAMPLES = 2
DEFAULT_MIN_SAMPLES = 2
DEFAULT_MIN_GRADIENT = 0.0
DEFAULT_SAMPLE_DURATION = 0

View File

@ -2,7 +2,9 @@
"domain": "trend", "domain": "trend",
"name": "Trend", "name": "Trend",
"codeowners": ["@jpbede"], "codeowners": ["@jpbede"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/trend", "documentation": "https://www.home-assistant.io/integrations/trend",
"integration_type": "helper",
"iot_class": "calculated", "iot_class": "calculated",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["numpy==1.26.0"] "requirements": ["numpy==1.26.0"]

View File

@ -4,5 +4,43 @@
"name": "[%key:common::action::reload%]", "name": "[%key:common::action::reload%]",
"description": "Reloads trend sensors from the YAML-configuration." "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."
}
}
}
} }
} }

View File

@ -14,6 +14,7 @@ FLOWS = {
"template", "template",
"threshold", "threshold",
"tod", "tod",
"trend",
"utility_meter", "utility_meter",
], ],
"integration": [ "integration": [

View File

@ -6113,12 +6113,6 @@
"config_flow": false, "config_flow": false,
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
}, },
"trend": {
"name": "Trend",
"integration_type": "hub",
"config_flow": false,
"iot_class": "calculated"
},
"tuya": { "tuya": {
"name": "Tuya", "name": "Tuya",
"integration_type": "hub", "integration_type": "hub",
@ -6944,6 +6938,12 @@
"config_flow": true, "config_flow": true,
"iot_class": "calculated" "iot_class": "calculated"
}, },
"trend": {
"name": "Trend",
"integration_type": "helper",
"config_flow": true,
"iot_class": "calculated"
},
"utility_meter": { "utility_meter": {
"integration_type": "helper", "integration_type": "helper",
"config_flow": true, "config_flow": true,

View 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

View File

@ -2,22 +2,22 @@
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from homeassistant import config as hass_config, setup from homeassistant import setup
from homeassistant.components.trend.const import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
from homeassistant.const import SERVICE_RELOAD, STATE_OFF, STATE_ON, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State from homeassistant.core import HomeAssistant, State
from homeassistant.setup import async_setup_component 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: async def _setup_legacy_component(hass: HomeAssistant, params: dict[str, Any]) -> None:
"""Set up the trend component.""" """Set up the trend component the legacy way."""
assert await async_setup_component( assert await async_setup_component(
hass, hass,
"binary_sensor", "binary_sensor",
@ -43,17 +43,54 @@ async def _setup_component(hass: HomeAssistant, params: dict[str, Any]) -> None:
], ],
ids=["up", "down", "up inverted", "down inverted"], ids=["up", "down", "up inverted", "down inverted"],
) )
async def test_basic_trend( async def test_basic_trend_setup_from_yaml(
hass: HomeAssistant, hass: HomeAssistant,
states: list[str], states: list[str],
inverted: bool, inverted: bool,
expected_state: str, expected_state: str,
): ) -> None:
"""Test trend with a basic setup.""" """Test trend with a basic setup."""
await _setup_component( await _setup_legacy_component(
hass, 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, "invert": inverted,
}, },
) )
@ -89,16 +126,16 @@ async def test_basic_trend(
) )
async def test_using_trendline( async def test_using_trendline(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
setup_component: ComponentSetup,
state_series: list[list[str]], state_series: list[list[str]],
inverted: bool, inverted: bool,
expected_states: list[str], expected_states: list[str],
): ) -> None:
"""Test uptrend using multiple samples and trendline calculation.""" """Test uptrend using multiple samples and trendline calculation."""
await _setup_component( await setup_component(
hass,
{ {
"entity_id": "sensor.test_state",
"sample_duration": 10000, "sample_duration": 10000,
"min_gradient": 1, "min_gradient": 1,
"max_samples": 25, "max_samples": 25,
@ -127,12 +164,13 @@ async def test_using_trendline(
) )
async def test_attribute_trend( async def test_attribute_trend(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry,
setup_component: ComponentSetup,
attr_values: list[str], attr_values: list[str],
expected_state: str, expected_state: str,
): ) -> None:
"""Test attribute uptrend.""" """Test attribute uptrend."""
await _setup_component( await setup_component(
hass,
{ {
"entity_id": "sensor.test_state", "entity_id": "sensor.test_state",
"attribute": "attr", "attribute": "attr",
@ -147,12 +185,12 @@ async def test_attribute_trend(
assert sensor_state.state == expected_state 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.""" """Test that sample count is limited correctly."""
await _setup_component( await setup_component(
hass,
{ {
"entity_id": "sensor.test_state",
"max_samples": 3, "max_samples": 3,
"min_gradient": -1, "min_gradient": -1,
}, },
@ -167,39 +205,39 @@ async def test_max_samples(hass: HomeAssistant):
assert state.attributes["sample_count"] == 3 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.""" """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") for val in ["Non", "Numeric"]:
await hass.async_block_till_done() hass.states.async_set("sensor.test_state", val)
hass.states.async_set("sensor.test_state", "Numeric") await hass.async_block_till_done()
await hass.async_block_till_done()
assert (state := hass.states.get("binary_sensor.test_trend_sensor")) assert (state := hass.states.get("binary_sensor.test_trend_sensor"))
assert state.state == STATE_UNKNOWN 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.""" """Test for missing attribute."""
await _setup_component( await setup_component(
hass,
{ {
"entity_id": "sensor.test_state",
"attribute": "missing", "attribute": "missing",
}, },
) )
hass.states.async_set("sensor.test_state", "State", {"attr": "2"}) for val in [1, 2]:
await hass.async_block_till_done() hass.states.async_set("sensor.test_state", "State", {"attr": val})
hass.states.async_set("sensor.test_state", "State", {"attr": "1"}) await hass.async_block_till_done()
await hass.async_block_till_done()
assert (state := hass.states.get("binary_sensor.test_trend_sensor")) assert (state := hass.states.get("binary_sensor.test_trend_sensor"))
assert state.state == STATE_UNKNOWN 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.""" """Test for invalid name."""
with assert_setup_component(0): with assert_setup_component(0):
assert await setup.async_setup_component( 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") == [] 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.""" """Test invalid sensor."""
with assert_setup_component(0): with assert_setup_component(0):
assert await setup.async_setup_component( 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") == [] 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.""" """Test no sensors."""
with assert_setup_component(0): with assert_setup_component(0):
assert await setup.async_setup_component( 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") == [] 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( @pytest.mark.parametrize(
("saved_state", "restored_state"), ("saved_state", "restored_state"),
[("on", "on"), ("off", "off"), ("unknown", "unknown")], [("on", "on"), ("off", "off"), ("unknown", "unknown")],
) )
async def test_restore_state( async def test_restore_state(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
setup_component: ComponentSetup,
saved_state: str, saved_state: str,
restored_state: str, restored_state: str,
) -> None: ) -> None:
"""Test we restore the trend state.""" """Test we restore the trend state."""
mock_restore_cache(hass, (State("binary_sensor.test_trend_sensor", saved_state),)) mock_restore_cache(hass, (State("binary_sensor.test_trend_sensor", saved_state),))
await _setup_component( await setup_component(
hass,
{ {
"entity_id": "sensor.test_state",
"sample_duration": 10000, "sample_duration": 10000,
"min_gradient": 1, "min_gradient": 1,
"max_samples": 25, "max_samples": 25,
@ -332,7 +334,7 @@ async def test_invalid_min_sample(
) -> None: ) -> None:
"""Test if error is logged when min_sample is larger than max_samples.""" """Test if error is logged when min_sample is larger than max_samples."""
with caplog.at_level(logging.ERROR): with caplog.at_level(logging.ERROR):
await _setup_component( await _setup_legacy_component(
hass, hass,
{ {
"entity_id": "sensor.test_state", "entity_id": "sensor.test_state",

View 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,
}

View 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}