From abbaaf4ff5f686b52fa8df25fa912e33aedf14e2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 12 Jul 2024 14:35:49 +0000 Subject: [PATCH 1/5] Add config flow to compensation helper --- .../components/compensation/__init__.py | 137 ++++++++++++------ .../components/compensation/config_flow.py | 132 +++++++++++++++++ .../components/compensation/const.py | 3 + .../components/compensation/manifest.json | 2 + .../components/compensation/sensor.py | 31 ++++ .../components/compensation/strings.json | 77 ++++++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 12 +- 8 files changed, 341 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/compensation/config_flow.py create mode 100644 homeassistant/components/compensation/strings.json diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index e83339d2c1854f..b705bce27c2932 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -7,15 +7,18 @@ import voluptuous as vol from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ATTRIBUTE, CONF_MAXIMUM, CONF_MINIMUM, + CONF_NAME, CONF_SOURCE, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.typing import ConfigType @@ -32,6 +35,7 @@ DEFAULT_DEGREE, DEFAULT_PRECISION, DOMAIN, + PLATFORMS, ) _LOGGER = logging.getLogger(__name__) @@ -77,59 +81,96 @@ def datapoints_greater_than_degree(value: dict) -> dict: ) +async def create_compensation_data( + hass: HomeAssistant, compensation: str, conf: ConfigType, should_raise: bool = False +) -> None: + """Create compensation data.""" + _LOGGER.debug("Setup %s.%s", DOMAIN, compensation) + + degree = conf[CONF_DEGREE] + + initial_coefficients: list[tuple[float, float]] = conf[CONF_DATAPOINTS] + sorted_coefficients = sorted(initial_coefficients, key=itemgetter(0)) + + # get x values and y values from the x,y point pairs + x_values, y_values = zip(*initial_coefficients, strict=False) + + # try to get valid coefficients for a polynomial + coefficients = None + with np.errstate(all="raise"): + try: + coefficients = np.polyfit(x_values, y_values, degree) + except FloatingPointError as error: + _LOGGER.error( + "Setup of %s encountered an error, %s", + compensation, + error, + ) + if should_raise: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="setup_error", + translation_placeholders={ + "title": conf[CONF_NAME], + "error": str(error), + }, + ) from error + + if coefficients is not None: + data = { + k: v for k, v in conf.items() if k not in [CONF_DEGREE, CONF_DATAPOINTS] + } + data[CONF_POLYNOMIAL] = np.poly1d(coefficients) + + if data[CONF_LOWER_LIMIT]: + data[CONF_MINIMUM] = sorted_coefficients[0] + else: + data[CONF_MINIMUM] = None + + if data[CONF_UPPER_LIMIT]: + data[CONF_MAXIMUM] = sorted_coefficients[-1] + else: + data[CONF_MAXIMUM] = None + + hass.data[DATA_COMPENSATION][compensation] = data + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Compensation sensor.""" hass.data[DATA_COMPENSATION] = {} + if DOMAIN not in config: + return True + for compensation, conf in config[DOMAIN].items(): - _LOGGER.debug("Setup %s.%s", DOMAIN, compensation) - - degree = conf[CONF_DEGREE] - - initial_coefficients: list[tuple[float, float]] = conf[CONF_DATAPOINTS] - sorted_coefficients = sorted(initial_coefficients, key=itemgetter(0)) - - # get x values and y values from the x,y point pairs - x_values, y_values = zip(*initial_coefficients, strict=False) - - # try to get valid coefficients for a polynomial - coefficients = None - with np.errstate(all="raise"): - try: - coefficients = np.polyfit(x_values, y_values, degree) - except FloatingPointError as error: - _LOGGER.error( - "Setup of %s encountered an error, %s", - compensation, - error, - ) - - if coefficients is not None: - data = { - k: v for k, v in conf.items() if k not in [CONF_DEGREE, CONF_DATAPOINTS] - } - data[CONF_POLYNOMIAL] = np.poly1d(coefficients) - - if data[CONF_LOWER_LIMIT]: - data[CONF_MINIMUM] = sorted_coefficients[0] - else: - data[CONF_MINIMUM] = None - - if data[CONF_UPPER_LIMIT]: - data[CONF_MAXIMUM] = sorted_coefficients[-1] - else: - data[CONF_MAXIMUM] = None - - hass.data[DATA_COMPENSATION][compensation] = data - - hass.async_create_task( - async_load_platform( - hass, - SENSOR_DOMAIN, - DOMAIN, - {CONF_COMPENSATION: compensation}, - config, - ) + await create_compensation_data(hass, compensation, conf) + hass.async_create_task( + async_load_platform( + hass, + SENSOR_DOMAIN, + DOMAIN, + {CONF_COMPENSATION: compensation}, + config, ) + ) return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Compensation from a config entry.""" + await create_compensation_data(hass, entry.entry_id, dict(entry.options), True) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Compensation config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/compensation/config_flow.py b/homeassistant/components/compensation/config_flow.py new file mode 100644 index 00000000000000..75cb56e8edb78b --- /dev/null +++ b/homeassistant/components/compensation/config_flow.py @@ -0,0 +1,132 @@ +"""Config flow for statistics.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import ( + CONF_ATTRIBUTE, + CONF_ENTITY_ID, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, +) +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, + SchemaConfigFlowHandler, + SchemaFlowError, + SchemaFlowFormStep, +) +from homeassistant.helpers.selector import ( + AttributeSelector, + AttributeSelectorConfig, + BooleanSelector, + EntitySelector, + NumberSelector, + NumberSelectorConfig, + NumberSelectorMode, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, + TextSelector, +) + +from .const import ( + CONF_DATAPOINTS, + CONF_DEGREE, + CONF_LOWER_LIMIT, + CONF_PRECISION, + CONF_UPPER_LIMIT, + DEFAULT_DEGREE, + DEFAULT_NAME, + DEFAULT_PRECISION, + DOMAIN, +) + + +async def get_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Get options schema.""" + entity_id = handler.options[CONF_ENTITY_ID] + + return vol.Schema( + { + vol.Required(CONF_DATAPOINTS): SelectSelector( + SelectSelectorConfig( + options=[], + multiple=True, + custom_value=True, + mode=SelectSelectorMode.DROPDOWN, + ) + ), + vol.Optional(CONF_ATTRIBUTE): AttributeSelector( + AttributeSelectorConfig(entity_id=entity_id) + ), + vol.Optional(CONF_UPPER_LIMIT, default=False): BooleanSelector(), + vol.Optional(CONF_LOWER_LIMIT, default=False): BooleanSelector(), + vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): NumberSelector( + NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX) + ), + vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): NumberSelector( + NumberSelectorConfig(min=0, max=7, step=1, mode=NumberSelectorMode.BOX) + ), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): TextSelector(), + } + ) + + +async def validate_options( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate options selected.""" + + user_input[CONF_PRECISION] = int(user_input[CONF_PRECISION]) + user_input[CONF_DEGREE] = int(user_input[CONF_DEGREE]) + + for datapoint in user_input[CONF_DATAPOINTS]: + if not isinstance(datapoint, list): + raise SchemaFlowError("incorrect_datapoints") + + if len(user_input[CONF_DATAPOINTS]) <= user_input[CONF_DEGREE]: + raise SchemaFlowError("not_enough_datapoints") + + handler.parent_handler._async_abort_entries_match({**handler.options, **user_input}) # noqa: SLF001 + + return user_input + + +DATA_SCHEMA_SETUP = vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): TextSelector(), + vol.Required(CONF_ENTITY_ID): EntitySelector(), + } +) + +CONFIG_FLOW = { + "user": SchemaFlowFormStep( + schema=DATA_SCHEMA_SETUP, + next_step="options", + ), + "options": SchemaFlowFormStep( + schema=get_options_schema, + validate_user_input=validate_options, + ), +} +OPTIONS_FLOW = { + "init": SchemaFlowFormStep( + get_options_schema, + validate_user_input=validate_options, + ), +} + + +class CompensationConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config flow for Compensation.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options[CONF_NAME]) diff --git a/homeassistant/components/compensation/const.py b/homeassistant/components/compensation/const.py index ce9594697006af..13b9740afa5f2d 100644 --- a/homeassistant/components/compensation/const.py +++ b/homeassistant/components/compensation/const.py @@ -1,6 +1,9 @@ """Compensation constants.""" +from homeassistant.const import Platform + DOMAIN = "compensation" +PLATFORMS = [Platform.SENSOR] SENSOR = "compensation" diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index eae58caa255362..7dfb27df783c62 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,9 @@ "domain": "compensation", "name": "Compensation", "codeowners": ["@Petro31"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/compensation", + "integration_type": "helper", "iot_class": "calculated", "quality_scale": "legacy", "requirements": ["numpy==2.3.0"] diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 956959325400e7..b8836f8dfa6bdc 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -8,6 +8,7 @@ import numpy as np from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_ATTRIBUTE, @@ -80,6 +81,36 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Compensation sensor entry.""" + compensation = entry.entry_id + conf: dict[str, Any] = hass.data[DATA_COMPENSATION][compensation] + + source: str = conf[CONF_SOURCE] + attribute: str | None = conf.get(CONF_ATTRIBUTE) + name = entry.title + + async_add_entities( + [ + CompensationSensor( + conf.get(CONF_UNIQUE_ID), + name, + source, + attribute, + conf[CONF_PRECISION], + conf[CONF_POLYNOMIAL], + conf.get(CONF_UNIT_OF_MEASUREMENT), + conf[CONF_MINIMUM], + conf[CONF_MAXIMUM], + ) + ] + ) + + class CompensationSensor(SensorEntity): """Representation of a Compensation sensor.""" diff --git a/homeassistant/components/compensation/strings.json b/homeassistant/components/compensation/strings.json new file mode 100644 index 00000000000000..568144b66c614f --- /dev/null +++ b/homeassistant/components/compensation/strings.json @@ -0,0 +1,77 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "incorrect_datapoints": "Datapoints needs to be provided in list-format, ex. '[1.0, 0.0]'.", + "not_enough_datapoints": "The number of datapoints needs to be less or equal to configured degree." + }, + "step": { + "user": { + "description": "Add a compensation sensor", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "entity_id": "Entity" + }, + "data_description": { + "name": "Name for the created entity.", + "entity_id": "Entity to use as source." + } + }, + "options": { + "description": "Read the documention for further details on how to configure the statistics sensor using these options.", + "data": { + "data_points": "Data points", + "attribute": "Attribute", + "upper_limit": "Upper limit", + "lower_limit": "Lower limit", + "precision": "Precision", + "degree": "Degree", + "unit_of_measurement": "Unit of measurement" + }, + "data_description": { + "data_points": "The collection of data point conversions with the format '[uncompensated_value, compensated_value]'", + "attribute": "Attribute from the source to monitor/compensate.", + "upper_limit": "Enables an upper limit for the sensor. The upper limit is defined by the data collections (data_points) greatest uncompensated value.", + "lower_limit": "Enables a lower limit for the sensor. The lower limit is defined by the data collections (data_points) lowest uncompensated value.", + "precision": "Defines the precision of the calculated values, through the argument of round().", + "degree": "The degree of a polynomial.", + "unit_of_measurement": "Defines the units of measurement of the sensor, if any." + } + } + } + }, + "options": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "incorrect_datapoints": "[%key:component::compensation::config::error::incorrect_datapoints%]", + "not_enough_datapoints": "[%key:component::compensation::config::error::not_enough_datapoints%]" + }, + "step": { + "init": { + "description": "[%key:component::compensation::config::step::options::description%]", + "data": { + "data_points": "[%key:component::compensation::config::step::options::data::data_points%]", + "attribute": "[%key:component::compensation::config::step::options::data::attribute%]", + "upper_limit": "[%key:component::compensation::config::step::options::data::upper_limit%]", + "lower_limit": "[%key:component::compensation::config::step::options::data::lower_limit%]", + "precision": "[%key:component::compensation::config::step::options::data::precision%]", + "degree": "[%key:component::compensation::config::step::options::data::degree%]", + "unit_of_measurement": "[%key:component::compensation::config::step::options::data::unit_of_measurement%]" + }, + "data_description": { + "data_points": "[%key:component::compensation::config::step::options::data_description::data_points%]", + "attribute": "[%key:component::compensation::config::step::options::data_description::attribute%]", + "upper_limit": "[%key:component::compensation::config::step::options::data_description::upper_limit%]", + "lower_limit": "[%key:component::compensation::config::step::options::data_description::lower_limit%]", + "precision": "[%key:component::compensation::config::step::options::data_description::precision%]", + "degree": "[%key:component::compensation::config::step::options::data_description::degree%]", + "unit_of_measurement": "[%key:component::compensation::config::step::options::data_description::unit_of_measurement%]" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 97e7929d3173df..8b7242e0782dd2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -5,6 +5,7 @@ FLOWS = { "helper": [ + "compensation", "derivative", "filter", "generic_hygrostat", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 6bf63b260de8d9..86516721a44d18 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1072,12 +1072,6 @@ "config_flow": false, "iot_class": "local_polling" }, - "compensation": { - "name": "Compensation", - "integration_type": "hub", - "config_flow": false, - "iot_class": "calculated" - }, "concord232": { "name": "Concord232", "integration_type": "hub", @@ -7733,6 +7727,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "compensation": { + "name": "Compensation", + "integration_type": "helper", + "config_flow": true, + "iot_class": "calculated" + }, "counter": { "integration_type": "helper", "config_flow": false From 4aebf41c5998785554818d94d67f403b7828e80e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 12 Jul 2024 15:05:09 +0000 Subject: [PATCH 2/5] Fixes --- .../components/compensation/__init__.py | 10 +++++++++- .../components/compensation/config_flow.py | 19 ++++++++++++++++--- .../components/compensation/sensor.py | 5 +++-- .../components/compensation/strings.json | 6 +++--- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index b705bce27c2932..1a352257cbdcb8 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -159,7 +159,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Compensation from a config entry.""" - await create_compensation_data(hass, entry.entry_id, dict(entry.options), True) + config = dict(entry.options) + data_points = config[CONF_DATAPOINTS] + new_data_points = [] + for data_point in data_points: + values = data_point.split(",", maxsplit=1) + new_data_points.append([float(values[0]), float(values[1])]) + config[CONF_DATAPOINTS] = new_data_points + + await create_compensation_data(hass, entry.entry_id, config, True) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/compensation/config_flow.py b/homeassistant/components/compensation/config_flow.py index 75cb56e8edb78b..09009002abe912 100644 --- a/homeassistant/components/compensation/config_flow.py +++ b/homeassistant/components/compensation/config_flow.py @@ -76,6 +76,20 @@ async def get_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: ) +def _is_valid_data_points(check_data_points: list[str]) -> bool: + """Validate data points.""" + for data_point in check_data_points: + if data_point.find(",") > 0: + values = data_point.split(",", maxsplit=1) + for value in values: + try: + float(value) + except ValueError: + return False + return True + return False + + async def validate_options( handler: SchemaCommonFlowHandler, user_input: dict[str, Any] ) -> dict[str, Any]: @@ -84,9 +98,8 @@ async def validate_options( user_input[CONF_PRECISION] = int(user_input[CONF_PRECISION]) user_input[CONF_DEGREE] = int(user_input[CONF_DEGREE]) - for datapoint in user_input[CONF_DATAPOINTS]: - if not isinstance(datapoint, list): - raise SchemaFlowError("incorrect_datapoints") + if not _is_valid_data_points(user_input[CONF_DATAPOINTS]): + raise SchemaFlowError("incorrect_datapoints") if len(user_input[CONF_DATAPOINTS]) <= user_input[CONF_DEGREE]: raise SchemaFlowError("not_enough_datapoints") diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index b8836f8dfa6bdc..b13b75fc42b655 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_ATTRIBUTE, + CONF_ENTITY_ID, CONF_MAXIMUM, CONF_MINIMUM, CONF_SOURCE, @@ -90,14 +91,14 @@ async def async_setup_entry( compensation = entry.entry_id conf: dict[str, Any] = hass.data[DATA_COMPENSATION][compensation] - source: str = conf[CONF_SOURCE] + source: str = conf[CONF_ENTITY_ID] attribute: str | None = conf.get(CONF_ATTRIBUTE) name = entry.title async_add_entities( [ CompensationSensor( - conf.get(CONF_UNIQUE_ID), + entry.entry_id, name, source, attribute, diff --git a/homeassistant/components/compensation/strings.json b/homeassistant/components/compensation/strings.json index 568144b66c614f..f7e428272fabae 100644 --- a/homeassistant/components/compensation/strings.json +++ b/homeassistant/components/compensation/strings.json @@ -4,8 +4,8 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { - "incorrect_datapoints": "Datapoints needs to be provided in list-format, ex. '[1.0, 0.0]'.", - "not_enough_datapoints": "The number of datapoints needs to be less or equal to configured degree." + "incorrect_datapoints": "Datapoints needs to be provided in the right format, ex. '1.0, 0.0'.", + "not_enough_datapoints": "The number of datapoints needs to be more than the configured degree." }, "step": { "user": { @@ -31,7 +31,7 @@ "unit_of_measurement": "Unit of measurement" }, "data_description": { - "data_points": "The collection of data point conversions with the format '[uncompensated_value, compensated_value]'", + "data_points": "The collection of data point conversions with the format 'uncompensated_value, compensated_value', ex. '1.0, 0.0'", "attribute": "Attribute from the source to monitor/compensate.", "upper_limit": "Enables an upper limit for the sensor. The upper limit is defined by the data collections (data_points) greatest uncompensated value.", "lower_limit": "Enables a lower limit for the sensor. The lower limit is defined by the data collections (data_points) lowest uncompensated value.", From 170989ef30393d939c2c08d8e73f8ea4d306c6c5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 12 Jul 2024 17:14:55 +0000 Subject: [PATCH 3/5] Fixes --- .../components/compensation/config_flow.py | 20 ++++++++++--------- .../components/compensation/strings.json | 5 +++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/compensation/config_flow.py b/homeassistant/components/compensation/config_flow.py index 09009002abe912..f00b90d978cf2a 100644 --- a/homeassistant/components/compensation/config_flow.py +++ b/homeassistant/components/compensation/config_flow.py @@ -78,16 +78,18 @@ async def get_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: def _is_valid_data_points(check_data_points: list[str]) -> bool: """Validate data points.""" + result = False for data_point in check_data_points: - if data_point.find(",") > 0: - values = data_point.split(",", maxsplit=1) - for value in values: - try: - float(value) - except ValueError: - return False - return True - return False + if not data_point.find(",") > 0: + return False + values = data_point.split(",", maxsplit=1) + for value in values: + try: + float(value) + except ValueError: + return False + result = True + return result async def validate_options( diff --git a/homeassistant/components/compensation/strings.json b/homeassistant/components/compensation/strings.json index f7e428272fabae..45753c5f6d3649 100644 --- a/homeassistant/components/compensation/strings.json +++ b/homeassistant/components/compensation/strings.json @@ -73,5 +73,10 @@ } } } + }, + "exceptions": { + "setup_error": { + "message": "Setup of {title} could not be setup due to {error}" + } } } From 612cc91423e0241354c7a3c5434d374dcc7d1522 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 12 Jul 2024 17:15:18 +0000 Subject: [PATCH 4/5] Add tests --- tests/components/compensation/conftest.py | 81 ++++++ .../compensation/test_config_flow.py | 266 ++++++++++++++++++ tests/components/compensation/test_init.py | 44 +++ tests/components/compensation/test_sensor.py | 33 +++ 4 files changed, 424 insertions(+) create mode 100644 tests/components/compensation/conftest.py create mode 100644 tests/components/compensation/test_config_flow.py create mode 100644 tests/components/compensation/test_init.py diff --git a/tests/components/compensation/conftest.py b/tests/components/compensation/conftest.py new file mode 100644 index 00000000000000..20bceadb35dd25 --- /dev/null +++ b/tests/components/compensation/conftest.py @@ -0,0 +1,81 @@ +"""Fixtures for the Compensation integration.""" + +from __future__ import annotations + +from collections.abc import Generator +from typing import Any +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.compensation.const import ( + CONF_DATAPOINTS, + CONF_DEGREE, + CONF_LOWER_LIMIT, + CONF_PRECISION, + CONF_UPPER_LIMIT, + DEFAULT_DEGREE, + DEFAULT_NAME, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Automatically patch compensation setup_entry.""" + with patch( + "homeassistant.components.compensation.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="get_config") +async def get_config_to_integration_load() -> dict[str, Any]: + """Return configuration. + + To override the config, tests can be marked with: + @pytest.mark.parametrize("get_config", [{...}]) + """ + return { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.uncompensated", + CONF_DATAPOINTS: [ + "1.0, 2.0", + "2.0, 3.0", + ], + CONF_UPPER_LIMIT: False, + CONF_LOWER_LIMIT: False, + CONF_PRECISION: 2, + CONF_DEGREE: DEFAULT_DEGREE, + CONF_UNIT_OF_MEASUREMENT: "mm", + } + + +@pytest.fixture(name="loaded_entry") +async def load_integration( + hass: HomeAssistant, get_config: dict[str, Any] +) -> MockConfigEntry: + """Set up the Compensation integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Compensation sensor", + source=SOURCE_USER, + options=get_config, + entry_id="1", + ) + config_entry.add_to_hass(hass) + + entity_id = get_config[CONF_ENTITY_ID] + hass.states.async_set(entity_id, 4, {}) + await hass.async_block_till_done() + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/compensation/test_config_flow.py b/tests/components/compensation/test_config_flow.py new file mode 100644 index 00000000000000..2639a6c0d0320b --- /dev/null +++ b/tests/components/compensation/test_config_flow.py @@ -0,0 +1,266 @@ +"""Test the Compensation config flow.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +from homeassistant import config_entries +from homeassistant.components.compensation.const import ( + CONF_DATAPOINTS, + CONF_DEGREE, + CONF_LOWER_LIMIT, + CONF_PRECISION, + CONF_UPPER_LIMIT, + DEFAULT_NAME, + DOMAIN, +) +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> 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"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_DATAPOINTS: [ + "1.0, 2.0", + "2.0, 3.0", + ], + CONF_UPPER_LIMIT: False, + CONF_LOWER_LIMIT: False, + CONF_PRECISION: 2, + CONF_DEGREE: 1, + CONF_UNIT_OF_MEASUREMENT: "mm", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["version"] == 1 + assert result["options"] == { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + CONF_DATAPOINTS: [ + "1.0, 2.0", + "2.0, 3.0", + ], + CONF_UPPER_LIMIT: False, + CONF_LOWER_LIMIT: False, + CONF_PRECISION: 2, + CONF_DEGREE: 1, + CONF_UNIT_OF_MEASUREMENT: "mm", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_options_flow(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: + """Test options flow.""" + + result = await hass.config_entries.options.async_init(loaded_entry.entry_id) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_DATAPOINTS: [ + "1.0, 2.0", + "2.0, 3.0", + ], + CONF_UPPER_LIMIT: False, + CONF_LOWER_LIMIT: False, + CONF_PRECISION: 2, + CONF_DEGREE: 1, + CONF_UNIT_OF_MEASUREMENT: "km", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.uncompensated", + CONF_DATAPOINTS: [ + "1.0, 2.0", + "2.0, 3.0", + ], + CONF_UPPER_LIMIT: False, + CONF_LOWER_LIMIT: False, + CONF_PRECISION: 2, + CONF_DEGREE: 1, + CONF_UNIT_OF_MEASUREMENT: "km", + } + + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 2 + + state = hass.states.get("sensor.compensation_sensor") + assert state is not None + + +async def test_validation_options( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test validation.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_DATAPOINTS: [ + "1.0, 2.0", + "2.0, 3.0", + ], + CONF_UPPER_LIMIT: False, + CONF_LOWER_LIMIT: False, + CONF_PRECISION: 2, + CONF_DEGREE: 2, + CONF_UNIT_OF_MEASUREMENT: "km", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "not_enough_datapoints"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_DATAPOINTS: [ + "1.0, 2.0", + "2.0 3.0", + ], + CONF_UPPER_LIMIT: False, + CONF_LOWER_LIMIT: False, + CONF_PRECISION: 2, + CONF_DEGREE: 1, + CONF_UNIT_OF_MEASUREMENT: "km", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "incorrect_datapoints"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_DATAPOINTS: [ + "1.0, 2.0", + "2,0, 3.0", + ], + CONF_UPPER_LIMIT: False, + CONF_LOWER_LIMIT: False, + CONF_PRECISION: 2, + CONF_DEGREE: 1, + CONF_UNIT_OF_MEASUREMENT: "km", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "incorrect_datapoints"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_DATAPOINTS: ["1.0, 2.0", "2.0, 3.0", "3.0, 4.0"], + CONF_UPPER_LIMIT: False, + CONF_LOWER_LIMIT: False, + CONF_PRECISION: 2, + CONF_DEGREE: 2, + CONF_UNIT_OF_MEASUREMENT: "km", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["version"] == 1 + assert result["options"] == { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + CONF_DATAPOINTS: ["1.0, 2.0", "2.0, 3.0", "3.0, 4.0"], + CONF_UPPER_LIMIT: False, + CONF_LOWER_LIMIT: False, + CONF_PRECISION: 2, + CONF_DEGREE: 2, + CONF_UNIT_OF_MEASUREMENT: "km", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_entry_already_exist( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """Test abort when entry already exist.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.uncompensated", + }, + ) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_DATAPOINTS: [ + "1.0, 2.0", + "2.0, 3.0", + ], + CONF_UPPER_LIMIT: False, + CONF_LOWER_LIMIT: False, + CONF_PRECISION: 2, + CONF_DEGREE: 1, + CONF_UNIT_OF_MEASUREMENT: "mm", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/compensation/test_init.py b/tests/components/compensation/test_init.py new file mode 100644 index 00000000000000..369f72f6aee893 --- /dev/null +++ b/tests/components/compensation/test_init.py @@ -0,0 +1,44 @@ +"""Test Statistics component setup process.""" + +from __future__ import annotations + +from typing import Any +from unittest.mock import patch + +from homeassistant.components.compensation.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: + """Test unload an entry.""" + + assert loaded_entry.state is ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(loaded_entry.entry_id) + await hass.async_block_till_done() + assert loaded_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_could_not_setup(hass: HomeAssistant, get_config: dict[str, Any]) -> None: + """Test exception.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Compensation sensor", + source=SOURCE_USER, + options=get_config, + entry_id="1", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.compensation.np.polyfit", + side_effect=FloatingPointError, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_ERROR + assert config_entry.error_reason_translation_key == "setup_error" diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index 877a4f972a9d4a..88bd1ccb47d2f5 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -1,5 +1,7 @@ """The tests for the integration sensor platform.""" +from typing import Any + import pytest from homeassistant.components.compensation.const import CONF_PRECISION, DOMAIN @@ -7,6 +9,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + CONF_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_STATE_CHANGED, STATE_UNKNOWN, @@ -14,6 +17,8 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + async def test_linear_state(hass: HomeAssistant) -> None: """Test compensation sensor state.""" @@ -60,6 +65,34 @@ async def test_linear_state(hass: HomeAssistant) -> None: assert state.state == STATE_UNKNOWN +async def test_linear_state_from_config_entry( + hass: HomeAssistant, loaded_entry: MockConfigEntry, get_config: dict[str, Any] +) -> None: + """Test compensation sensor state loaded from config entry.""" + expected_entity_id = "sensor.compensation_sensor" + entity_id = get_config[CONF_ENTITY_ID] + + hass.states.async_set(entity_id, 5, {}) + await hass.async_block_till_done() + + state = hass.states.get(expected_entity_id) + assert state is not None + assert round(float(state.state), get_config[CONF_PRECISION]) == 6.0 + + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "mm" + + coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)] + assert coefs == [1.0, 1.0] + + hass.states.async_set(entity_id, "foo", {}) + await hass.async_block_till_done() + + state = hass.states.get(expected_entity_id) + assert state is not None + + assert state.state == STATE_UNKNOWN + + async def test_linear_state_from_attribute(hass: HomeAssistant) -> None: """Test compensation sensor state that pulls from attribute.""" config = { From ed68a21afd148bf8230ca605e043c94bd726c86a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 12 Jul 2024 17:20:04 +0000 Subject: [PATCH 5/5] Don't load from platform yaml --- tests/components/compensation/test_sensor.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index 88bd1ccb47d2f5..4d9362bc64682d 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_ENTITY_ID, + CONF_PLATFORM, EVENT_HOMEASSISTANT_START, EVENT_STATE_CHANGED, STATE_UNKNOWN, @@ -20,6 +21,22 @@ from tests.common import MockConfigEntry +async def test_not_loading_from_platform_yaml(hass: HomeAssistant) -> None: + """Test compensation sensor not loaded from platform YAML.""" + config = { + "sensor": [ + { + CONF_PLATFORM: DOMAIN, + } + ] + } + + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + async def test_linear_state(hass: HomeAssistant) -> None: """Test compensation sensor state.""" config = { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy