diff --git a/homeassistant/components/trend/__init__.py b/homeassistant/components/trend/__init__.py index b583f424da1..91d50bcc928 100644 --- a/homeassistant/components/trend/__init__.py +++ b/homeassistant/components/trend/__init__.py @@ -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) diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index fa6ad8e5382..c86fb65e966 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -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: diff --git a/homeassistant/components/trend/config_flow.py b/homeassistant/components/trend/config_flow.py new file mode 100644 index 00000000000..457522dca82 --- /dev/null +++ b/homeassistant/components/trend/config_flow.py @@ -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]) diff --git a/homeassistant/components/trend/const.py b/homeassistant/components/trend/const.py index 3d82bfcc648..838056bfc4d 100644 --- a/homeassistant/components/trend/const.py +++ b/homeassistant/components/trend/const.py @@ -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 diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 0adbf623346..110bab99e52 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -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"] diff --git a/homeassistant/components/trend/strings.json b/homeassistant/components/trend/strings.json index 6af231bb4c5..2fe0b35ee3c 100644 --- a/homeassistant/components/trend/strings.json +++ b/homeassistant/components/trend/strings.json @@ -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." + } + } + } } } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7da240ac266..cba1a88d25b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -14,6 +14,7 @@ FLOWS = { "template", "threshold", "tod", + "trend", "utility_meter", ], "integration": [ diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3a1e154facb..995609ec226 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -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, diff --git a/tests/components/trend/conftest.py b/tests/components/trend/conftest.py new file mode 100644 index 00000000000..cff3831658a --- /dev/null +++ b/tests/components/trend/conftest.py @@ -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 diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index b525c7a8fa3..115bac5ed5d 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -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", diff --git a/tests/components/trend/test_config_flow.py b/tests/components/trend/test_config_flow.py new file mode 100644 index 00000000000..e81d57ef9e1 --- /dev/null +++ b/tests/components/trend/test_config_flow.py @@ -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, + } diff --git a/tests/components/trend/test_init.py b/tests/components/trend/test_init.py new file mode 100644 index 00000000000..47bcab2214d --- /dev/null +++ b/tests/components/trend/test_init.py @@ -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}