mirror of
https://github.com/home-assistant/core.git
synced 2025-10-04 09:19:28 +00:00
Compare commits
5 Commits
2025.10.1
...
compensati
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ed68a21afd | ||
![]() |
612cc91423 | ||
![]() |
170989ef30 | ||
![]() |
4aebf41c59 | ||
![]() |
abbaaf4ff5 |
@@ -7,15 +7,18 @@ import numpy as np
|
||||
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 @@ from .const import (
|
||||
DEFAULT_DEGREE,
|
||||
DEFAULT_PRECISION,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -77,59 +81,104 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
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."""
|
||||
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))
|
||||
|
||||
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)
|
||||
|
147
homeassistant/components/compensation/config_flow.py
Normal file
147
homeassistant/components/compensation/config_flow.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""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(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _is_valid_data_points(check_data_points: list[str]) -> bool:
|
||||
"""Validate data points."""
|
||||
result = False
|
||||
for data_point in check_data_points:
|
||||
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(
|
||||
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])
|
||||
|
||||
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")
|
||||
|
||||
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])
|
@@ -1,6 +1,9 @@
|
||||
"""Compensation constants."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "compensation"
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
SENSOR = "compensation"
|
||||
|
||||
|
@@ -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"]
|
||||
|
@@ -8,9 +8,11 @@ from typing import Any
|
||||
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,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_MAXIMUM,
|
||||
CONF_MINIMUM,
|
||||
CONF_SOURCE,
|
||||
@@ -80,6 +82,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_ENTITY_ID]
|
||||
attribute: str | None = conf.get(CONF_ATTRIBUTE)
|
||||
name = entry.title
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
CompensationSensor(
|
||||
entry.entry_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."""
|
||||
|
||||
|
82
homeassistant/components/compensation/strings.json
Normal file
82
homeassistant/components/compensation/strings.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"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": {
|
||||
"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', 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.",
|
||||
"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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"setup_error": {
|
||||
"message": "Setup of {title} could not be setup due to {error}"
|
||||
}
|
||||
}
|
||||
}
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -5,6 +5,7 @@ To update, run python3 -m script.hassfest
|
||||
|
||||
FLOWS = {
|
||||
"helper": [
|
||||
"compensation",
|
||||
"derivative",
|
||||
"filter",
|
||||
"generic_hygrostat",
|
||||
|
@@ -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
|
||||
|
81
tests/components/compensation/conftest.py
Normal file
81
tests/components/compensation/conftest.py
Normal file
@@ -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
|
266
tests/components/compensation/test_config_flow.py
Normal file
266
tests/components/compensation/test_config_flow.py
Normal file
@@ -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"
|
44
tests/components/compensation/test_init.py
Normal file
44
tests/components/compensation/test_init.py
Normal file
@@ -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"
|
@@ -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,8 @@ from homeassistant.components.compensation.sensor import ATTR_COEFFICIENTS
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_PLATFORM,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_STATE_CHANGED,
|
||||
STATE_UNKNOWN,
|
||||
@@ -14,6 +18,24 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
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."""
|
||||
@@ -60,6 +82,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 = {
|
||||
|
Reference in New Issue
Block a user