mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
Fix missing device_class and state_class on compensation entities (#146115)
Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
parent
73251fbb1c
commit
cb8e076703
@ -6,11 +6,18 @@ from operator import itemgetter
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
from homeassistant.components.sensor import (
|
||||||
|
CONF_STATE_CLASS,
|
||||||
|
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||||
|
DOMAIN as SENSOR_DOMAIN,
|
||||||
|
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ATTRIBUTE,
|
CONF_ATTRIBUTE,
|
||||||
|
CONF_DEVICE_CLASS,
|
||||||
CONF_MAXIMUM,
|
CONF_MAXIMUM,
|
||||||
CONF_MINIMUM,
|
CONF_MINIMUM,
|
||||||
|
CONF_NAME,
|
||||||
CONF_SOURCE,
|
CONF_SOURCE,
|
||||||
CONF_UNIQUE_ID,
|
CONF_UNIQUE_ID,
|
||||||
CONF_UNIT_OF_MEASUREMENT,
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
@ -50,20 +57,23 @@ def datapoints_greater_than_degree(value: dict) -> dict:
|
|||||||
|
|
||||||
COMPENSATION_SCHEMA = vol.Schema(
|
COMPENSATION_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_SOURCE): cv.entity_id,
|
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
||||||
vol.Required(CONF_DATAPOINTS): [
|
vol.Required(CONF_DATAPOINTS): [
|
||||||
vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)])
|
vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)])
|
||||||
],
|
],
|
||||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
|
||||||
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
|
||||||
vol.Optional(CONF_UPPER_LIMIT, default=False): cv.boolean,
|
|
||||||
vol.Optional(CONF_LOWER_LIMIT, default=False): cv.boolean,
|
|
||||||
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int,
|
|
||||||
vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All(
|
vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All(
|
||||||
vol.Coerce(int),
|
vol.Coerce(int),
|
||||||
vol.Range(min=1, max=7),
|
vol.Range(min=1, max=7),
|
||||||
),
|
),
|
||||||
|
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||||
|
vol.Optional(CONF_LOWER_LIMIT, default=False): cv.boolean,
|
||||||
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int,
|
||||||
|
vol.Required(CONF_SOURCE): cv.entity_id,
|
||||||
|
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
|
||||||
|
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||||
|
vol.Optional(CONF_UPPER_LIMIT, default=False): cv.boolean,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -7,15 +7,23 @@ from typing import Any
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorEntity
|
from homeassistant.components.sensor import (
|
||||||
|
ATTR_STATE_CLASS,
|
||||||
|
CONF_STATE_CLASS,
|
||||||
|
SensorEntity,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
CONF_ATTRIBUTE,
|
CONF_ATTRIBUTE,
|
||||||
|
CONF_DEVICE_CLASS,
|
||||||
CONF_MAXIMUM,
|
CONF_MAXIMUM,
|
||||||
CONF_MINIMUM,
|
CONF_MINIMUM,
|
||||||
|
CONF_NAME,
|
||||||
CONF_SOURCE,
|
CONF_SOURCE,
|
||||||
CONF_UNIQUE_ID,
|
CONF_UNIQUE_ID,
|
||||||
CONF_UNIT_OF_MEASUREMENT,
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
@ -59,24 +67,13 @@ async def async_setup_platform(
|
|||||||
|
|
||||||
source: str = conf[CONF_SOURCE]
|
source: str = conf[CONF_SOURCE]
|
||||||
attribute: str | None = conf.get(CONF_ATTRIBUTE)
|
attribute: str | None = conf.get(CONF_ATTRIBUTE)
|
||||||
|
if not (name := conf.get(CONF_NAME)):
|
||||||
name = f"{DEFAULT_NAME} {source}"
|
name = f"{DEFAULT_NAME} {source}"
|
||||||
if attribute is not None:
|
if attribute is not None:
|
||||||
name = f"{name} {attribute}"
|
name = f"{name} {attribute}"
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[CompensationSensor(conf.get(CONF_UNIQUE_ID), name, source, attribute, conf)]
|
||||||
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],
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -91,23 +88,27 @@ class CompensationSensor(SensorEntity):
|
|||||||
name: str,
|
name: str,
|
||||||
source: str,
|
source: str,
|
||||||
attribute: str | None,
|
attribute: str | None,
|
||||||
precision: int,
|
config: dict[str, Any],
|
||||||
polynomial: np.poly1d,
|
|
||||||
unit_of_measurement: str | None,
|
|
||||||
minimum: tuple[float, float] | None,
|
|
||||||
maximum: tuple[float, float] | None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Compensation sensor."""
|
"""Initialize the Compensation sensor."""
|
||||||
|
|
||||||
|
self._attr_name = name
|
||||||
self._source_entity_id = source
|
self._source_entity_id = source
|
||||||
self._precision = precision
|
|
||||||
self._source_attribute = attribute
|
self._source_attribute = attribute
|
||||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
|
||||||
|
self._precision = config[CONF_PRECISION]
|
||||||
|
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||||
|
|
||||||
|
polynomial: np.poly1d = config[CONF_POLYNOMIAL]
|
||||||
self._poly = polynomial
|
self._poly = polynomial
|
||||||
self._coefficients = polynomial.coefficients.tolist()
|
self._coefficients = polynomial.coefficients.tolist()
|
||||||
|
|
||||||
self._attr_unique_id = unique_id
|
self._attr_unique_id = unique_id
|
||||||
self._attr_name = name
|
self._minimum = config[CONF_MINIMUM]
|
||||||
self._minimum = minimum
|
self._maximum = config[CONF_MAXIMUM]
|
||||||
self._maximum = maximum
|
|
||||||
|
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
|
||||||
|
self._attr_state_class = config.get(CONF_STATE_CLASS)
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Handle added to Hass."""
|
"""Handle added to Hass."""
|
||||||
@ -137,13 +138,40 @@ class CompensationSensor(SensorEntity):
|
|||||||
"""Handle sensor state changes."""
|
"""Handle sensor state changes."""
|
||||||
new_state: State | None
|
new_state: State | None
|
||||||
if (new_state := event.data["new_state"]) is None:
|
if (new_state := event.data["new_state"]) is None:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"While updating compensation %s, the new_state is None", self.name
|
||||||
|
)
|
||||||
|
self._attr_native_value = None
|
||||||
|
self.async_write_ha_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if new_state.state == STATE_UNKNOWN:
|
||||||
|
self._attr_native_value = None
|
||||||
|
self.async_write_ha_state()
|
||||||
|
return
|
||||||
|
|
||||||
|
if new_state.state == STATE_UNAVAILABLE:
|
||||||
|
self._attr_available = False
|
||||||
|
self.async_write_ha_state()
|
||||||
|
return
|
||||||
|
|
||||||
|
self._attr_available = True
|
||||||
|
|
||||||
if self.native_unit_of_measurement is None and self._source_attribute is None:
|
if self.native_unit_of_measurement is None and self._source_attribute is None:
|
||||||
self._attr_native_unit_of_measurement = new_state.attributes.get(
|
self._attr_native_unit_of_measurement = new_state.attributes.get(
|
||||||
ATTR_UNIT_OF_MEASUREMENT
|
ATTR_UNIT_OF_MEASUREMENT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self._attr_device_class is None and (
|
||||||
|
device_class := new_state.attributes.get(ATTR_DEVICE_CLASS)
|
||||||
|
):
|
||||||
|
self._attr_device_class = device_class
|
||||||
|
|
||||||
|
if self._attr_state_class is None and (
|
||||||
|
state_class := new_state.attributes.get(ATTR_STATE_CLASS)
|
||||||
|
):
|
||||||
|
self._attr_state_class = state_class
|
||||||
|
|
||||||
if self._source_attribute:
|
if self._source_attribute:
|
||||||
value = new_state.attributes.get(self._source_attribute)
|
value = new_state.attributes.get(self._source_attribute)
|
||||||
else:
|
else:
|
||||||
|
@ -1,115 +1,181 @@
|
|||||||
"""The tests for the integration sensor platform."""
|
"""The tests for the integration sensor platform."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant import config as hass_config
|
||||||
from homeassistant.components.compensation.const import CONF_PRECISION, DOMAIN
|
from homeassistant.components.compensation.const import CONF_PRECISION, DOMAIN
|
||||||
from homeassistant.components.compensation.sensor import ATTR_COEFFICIENTS
|
from homeassistant.components.compensation.sensor import ATTR_COEFFICIENTS
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
from homeassistant.components.sensor import (
|
||||||
|
ATTR_STATE_CLASS,
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
EVENT_HOMEASSISTANT_START,
|
EVENT_HOMEASSISTANT_START,
|
||||||
EVENT_STATE_CHANGED,
|
EVENT_STATE_CHANGED,
|
||||||
|
SERVICE_RELOAD,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import assert_setup_component, get_fixture_path
|
||||||
|
|
||||||
async def test_linear_state(hass: HomeAssistant) -> None:
|
TEST_OBJECT_ID = "test_compensation"
|
||||||
"""Test compensation sensor state."""
|
TEST_ENTITY_ID = "sensor.test_compensation"
|
||||||
config = {
|
TEST_SOURCE = "sensor.uncompensated"
|
||||||
"compensation": {
|
|
||||||
"test": {
|
TEST_BASE_CONFIG = {
|
||||||
"source": "sensor.uncompensated",
|
"source": TEST_SOURCE,
|
||||||
"data_points": [
|
"data_points": [
|
||||||
[1.0, 2.0],
|
[1.0, 2.0],
|
||||||
[2.0, 3.0],
|
[2.0, 3.0],
|
||||||
],
|
],
|
||||||
"precision": 2,
|
"precision": 2,
|
||||||
|
}
|
||||||
|
TEST_CONFIG = {
|
||||||
|
"name": TEST_OBJECT_ID,
|
||||||
"unit_of_measurement": "a",
|
"unit_of_measurement": "a",
|
||||||
}
|
**TEST_BASE_CONFIG,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
expected_entity_id = "sensor.compensation_sensor_uncompensated"
|
|
||||||
|
|
||||||
assert await async_setup_component(hass, DOMAIN, config)
|
|
||||||
assert await async_setup_component(hass, SENSOR_DOMAIN, config)
|
async def async_setup_compensation(hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||||
|
"""Do setup of a compensation integration sensor."""
|
||||||
|
with assert_setup_component(1, DOMAIN):
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test": config}},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def setup_compensation(hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||||
|
"""Do setup of a compensation integration sensor."""
|
||||||
|
await async_setup_compensation(hass, config)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def setup_compensation_with_limits(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: dict[str, Any],
|
||||||
|
upper: bool,
|
||||||
|
lower: bool,
|
||||||
|
):
|
||||||
|
"""Do setup of a compensation integration sensor with extra config."""
|
||||||
|
await async_setup_compensation(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
**config,
|
||||||
|
"lower_limit": lower,
|
||||||
|
"upper_limit": upper,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def caplog_setup_text(caplog: pytest.LogCaptureFixture) -> str:
|
||||||
|
"""Return setup log of integration."""
|
||||||
|
return caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("config", [TEST_CONFIG])
|
||||||
|
@pytest.mark.usefixtures("setup_compensation")
|
||||||
|
async def test_linear_state(hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||||
|
"""Test compensation sensor state."""
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
entity_id = config[DOMAIN]["test"]["source"]
|
hass.states.async_set(TEST_SOURCE, 4, {})
|
||||||
hass.states.async_set(entity_id, 4, {})
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
state = hass.states.get(expected_entity_id)
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
assert state is not None
|
assert state is not None
|
||||||
|
|
||||||
assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 5.0
|
assert round(float(state.state), config[CONF_PRECISION]) == 5.0
|
||||||
|
|
||||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "a"
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "a"
|
||||||
|
|
||||||
coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)]
|
coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)]
|
||||||
assert coefs == [1.0, 1.0]
|
assert coefs == [1.0, 1.0]
|
||||||
|
|
||||||
hass.states.async_set(entity_id, "foo", {})
|
hass.states.async_set(TEST_SOURCE, "foo", {})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
state = hass.states.get(expected_entity_id)
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
assert state is not None
|
assert state is not None
|
||||||
|
|
||||||
assert state.state == STATE_UNKNOWN
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
async def test_linear_state_from_attribute(hass: HomeAssistant) -> None:
|
@pytest.mark.parametrize("config", [{"name": TEST_OBJECT_ID, **TEST_BASE_CONFIG}])
|
||||||
"""Test compensation sensor state that pulls from attribute."""
|
@pytest.mark.usefixtures("setup_compensation")
|
||||||
config = {
|
async def test_attributes_come_from_source(hass: HomeAssistant) -> None:
|
||||||
"compensation": {
|
"""Test compensation sensor state."""
|
||||||
"test": {
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
"source": "sensor.uncompensated",
|
hass.states.async_set(
|
||||||
"attribute": "value",
|
TEST_SOURCE,
|
||||||
"data_points": [
|
4,
|
||||||
[1.0, 2.0],
|
{
|
||||||
[2.0, 3.0],
|
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
|
||||||
],
|
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
|
||||||
"precision": 2,
|
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||||
}
|
},
|
||||||
}
|
)
|
||||||
}
|
|
||||||
expected_entity_id = "sensor.compensation_sensor_uncompensated_value"
|
|
||||||
|
|
||||||
assert await async_setup_component(hass, DOMAIN, config)
|
|
||||||
assert await async_setup_component(hass, SENSOR_DOMAIN, config)
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == "5.0"
|
||||||
|
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
|
||||||
|
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS
|
||||||
|
assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("config", [{"attribute": "value", **TEST_CONFIG}])
|
||||||
|
@pytest.mark.usefixtures("setup_compensation")
|
||||||
|
async def test_linear_state_from_attribute(
|
||||||
|
hass: HomeAssistant, config: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Test compensation sensor state that pulls from attribute."""
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
|
||||||
entity_id = config[DOMAIN]["test"]["source"]
|
hass.states.async_set(TEST_SOURCE, 3, {"value": 4})
|
||||||
hass.states.async_set(entity_id, 3, {"value": 4})
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
state = hass.states.get(expected_entity_id)
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
assert state is not None
|
assert state is not None
|
||||||
|
|
||||||
assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 5.0
|
assert round(float(state.state), config[CONF_PRECISION]) == 5.0
|
||||||
|
|
||||||
coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)]
|
coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)]
|
||||||
assert coefs == [1.0, 1.0]
|
assert coefs == [1.0, 1.0]
|
||||||
|
|
||||||
hass.states.async_set(entity_id, 3, {"value": "bar"})
|
hass.states.async_set(TEST_SOURCE, 3, {"value": "bar"})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
state = hass.states.get(expected_entity_id)
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
assert state is not None
|
assert state is not None
|
||||||
|
|
||||||
assert state.state == STATE_UNKNOWN
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
async def test_quadratic_state(hass: HomeAssistant) -> None:
|
@pytest.mark.parametrize(
|
||||||
"""Test 3 degree polynominial compensation sensor."""
|
"config",
|
||||||
config = {
|
[
|
||||||
"compensation": {
|
{
|
||||||
"test": {
|
"name": TEST_OBJECT_ID,
|
||||||
"source": "sensor.temperature",
|
"source": TEST_SOURCE,
|
||||||
"data_points": [
|
"data_points": [
|
||||||
[50, 3.3],
|
[50, 3.3],
|
||||||
[50, 2.8],
|
[50, 2.8],
|
||||||
@ -129,46 +195,38 @@ async def test_quadratic_state(hass: HomeAssistant) -> None:
|
|||||||
],
|
],
|
||||||
"degree": 2,
|
"degree": 2,
|
||||||
"precision": 3,
|
"precision": 3,
|
||||||
}
|
},
|
||||||
}
|
],
|
||||||
}
|
)
|
||||||
assert await async_setup_component(hass, DOMAIN, config)
|
@pytest.mark.usefixtures("setup_compensation")
|
||||||
await hass.async_block_till_done()
|
async def test_quadratic_state(hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||||
await hass.async_start()
|
"""Test 3 degree polynominial compensation sensor."""
|
||||||
|
hass.states.async_set(TEST_SOURCE, 43.2, {})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
entity_id = config[DOMAIN]["test"]["source"]
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
hass.states.async_set(entity_id, 43.2, {})
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
state = hass.states.get("sensor.compensation_sensor_temperature")
|
|
||||||
|
|
||||||
assert state is not None
|
assert state is not None
|
||||||
|
|
||||||
assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 3.327
|
assert round(float(state.state), config[CONF_PRECISION]) == 3.327
|
||||||
|
|
||||||
|
|
||||||
async def test_numpy_errors(
|
@pytest.mark.parametrize(
|
||||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
"config",
|
||||||
) -> None:
|
[
|
||||||
"""Tests bad polyfits."""
|
{
|
||||||
config = {
|
"source": TEST_SOURCE,
|
||||||
"compensation": {
|
|
||||||
"test": {
|
|
||||||
"source": "sensor.uncompensated",
|
|
||||||
"data_points": [
|
"data_points": [
|
||||||
[0.0, 1.0],
|
[0.0, 1.0],
|
||||||
[0.0, 1.0],
|
[0.0, 1.0],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
],
|
||||||
}
|
)
|
||||||
await async_setup_component(hass, DOMAIN, config)
|
@pytest.mark.usefixtures("setup_compensation")
|
||||||
await hass.async_block_till_done()
|
async def test_numpy_errors(hass: HomeAssistant, caplog_setup_text) -> None:
|
||||||
await hass.async_start()
|
"""Tests bad polyfits."""
|
||||||
await hass.async_block_till_done()
|
assert "invalid value encountered in divide" in caplog_setup_text
|
||||||
|
|
||||||
assert "invalid value encountered in divide" in caplog.text
|
|
||||||
|
|
||||||
|
|
||||||
async def test_datapoints_greater_than_degree(
|
async def test_datapoints_greater_than_degree(
|
||||||
@ -178,7 +236,7 @@ async def test_datapoints_greater_than_degree(
|
|||||||
config = {
|
config = {
|
||||||
"compensation": {
|
"compensation": {
|
||||||
"test": {
|
"test": {
|
||||||
"source": "sensor.uncompensated",
|
"source": TEST_SOURCE,
|
||||||
"data_points": [
|
"data_points": [
|
||||||
[1.0, 2.0],
|
[1.0, 2.0],
|
||||||
[2.0, 3.0],
|
[2.0, 3.0],
|
||||||
@ -195,35 +253,13 @@ async def test_datapoints_greater_than_degree(
|
|||||||
assert "data_points must have at least 3 data_points" in caplog.text
|
assert "data_points must have at least 3 data_points" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("config", [TEST_CONFIG])
|
||||||
|
@pytest.mark.usefixtures("setup_compensation")
|
||||||
async def test_new_state_is_none(hass: HomeAssistant) -> None:
|
async def test_new_state_is_none(hass: HomeAssistant) -> None:
|
||||||
"""Tests catch for empty new states."""
|
"""Tests catch for empty new states."""
|
||||||
config = {
|
last_changed = hass.states.get(TEST_ENTITY_ID).last_changed
|
||||||
"compensation": {
|
hass.bus.async_fire(EVENT_STATE_CHANGED, event_data={"entity_id": TEST_SOURCE})
|
||||||
"test": {
|
assert last_changed == hass.states.get(TEST_ENTITY_ID).last_changed
|
||||||
"source": "sensor.uncompensated",
|
|
||||||
"data_points": [
|
|
||||||
[1.0, 2.0],
|
|
||||||
[2.0, 3.0],
|
|
||||||
],
|
|
||||||
"precision": 2,
|
|
||||||
"unit_of_measurement": "a",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expected_entity_id = "sensor.compensation_sensor_uncompensated"
|
|
||||||
|
|
||||||
await async_setup_component(hass, DOMAIN, config)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
await hass.async_start()
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
last_changed = hass.states.get(expected_entity_id).last_changed
|
|
||||||
|
|
||||||
hass.bus.async_fire(
|
|
||||||
EVENT_STATE_CHANGED, event_data={"entity_id": "sensor.uncompensated"}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert last_changed == hass.states.get(expected_entity_id).last_changed
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -234,40 +270,129 @@ async def test_new_state_is_none(hass: HomeAssistant) -> None:
|
|||||||
(True, True),
|
(True, True),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_limits(hass: HomeAssistant, lower: bool, upper: bool) -> None:
|
@pytest.mark.parametrize(
|
||||||
"""Test compensation sensor state."""
|
"config",
|
||||||
source = "sensor.test"
|
[
|
||||||
config = {
|
{
|
||||||
"compensation": {
|
"name": TEST_OBJECT_ID,
|
||||||
"test": {
|
"source": TEST_SOURCE,
|
||||||
"source": source,
|
|
||||||
"data_points": [
|
"data_points": [
|
||||||
[1.0, 0.0],
|
[1.0, 0.0],
|
||||||
[3.0, 2.0],
|
[3.0, 2.0],
|
||||||
[2.0, 1.0],
|
[2.0, 1.0],
|
||||||
],
|
],
|
||||||
"precision": 2,
|
"precision": 2,
|
||||||
"lower_limit": lower,
|
|
||||||
"upper_limit": upper,
|
|
||||||
"unit_of_measurement": "a",
|
"unit_of_measurement": "a",
|
||||||
}
|
},
|
||||||
}
|
],
|
||||||
}
|
)
|
||||||
await async_setup_component(hass, DOMAIN, config)
|
@pytest.mark.usefixtures("setup_compensation_with_limits")
|
||||||
|
async def test_limits(hass: HomeAssistant, lower: bool, upper: bool) -> None:
|
||||||
|
"""Test compensation sensor state."""
|
||||||
|
hass.states.async_set(TEST_SOURCE, 0, {})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await hass.async_start()
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
entity_id = "sensor.compensation_sensor_test"
|
|
||||||
|
|
||||||
hass.states.async_set(source, 0, {})
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
state = hass.states.get(entity_id)
|
|
||||||
value = 0.0 if lower else -1.0
|
value = 0.0 if lower else -1.0
|
||||||
assert float(state.state) == value
|
assert float(state.state) == value
|
||||||
|
|
||||||
hass.states.async_set(source, 5, {})
|
hass.states.async_set(TEST_SOURCE, 5, {})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
value = 2.0 if upper else 4.0
|
value = 2.0 if upper else 4.0
|
||||||
assert float(state.state) == value
|
assert float(state.state) == value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("config", "expected"),
|
||||||
|
[
|
||||||
|
(TEST_BASE_CONFIG, "sensor.compensation_sensor_uncompensated"),
|
||||||
|
(
|
||||||
|
{"attribute": "value", **TEST_BASE_CONFIG},
|
||||||
|
"sensor.compensation_sensor_uncompensated_value",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.usefixtures("setup_compensation")
|
||||||
|
async def test_default_name(hass: HomeAssistant, expected: str) -> None:
|
||||||
|
"""Test default configuration name."""
|
||||||
|
assert hass.states.get(expected) is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("config", [TEST_CONFIG])
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("source_state", "expected"),
|
||||||
|
[(STATE_UNKNOWN, STATE_UNKNOWN), (STATE_UNAVAILABLE, STATE_UNAVAILABLE)],
|
||||||
|
)
|
||||||
|
@pytest.mark.usefixtures("setup_compensation")
|
||||||
|
async def test_non_numerical_states_from_source_entity(
|
||||||
|
hass: HomeAssistant, config: dict[str, Any], source_state: str, expected: str
|
||||||
|
) -> None:
|
||||||
|
"""Test non-numerical states from source entity."""
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
hass.states.async_set(TEST_SOURCE, source_state)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == expected
|
||||||
|
|
||||||
|
hass.states.async_set(TEST_SOURCE, 4)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
|
assert state is not None
|
||||||
|
assert round(float(state.state), config[CONF_PRECISION]) == 5.0
|
||||||
|
|
||||||
|
hass.states.async_set(TEST_SOURCE, source_state)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == expected
|
||||||
|
|
||||||
|
|
||||||
|
async def test_source_state_none(hass: HomeAssistant) -> None:
|
||||||
|
"""Test is source sensor state is null and sets state to STATE_UNKNOWN."""
|
||||||
|
config = {
|
||||||
|
"sensor": [
|
||||||
|
{
|
||||||
|
"platform": "template",
|
||||||
|
"sensors": {
|
||||||
|
"uncompensated": {
|
||||||
|
"value_template": "{{ states.sensor.test_state.state }}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
await async_setup_component(hass, "sensor", config)
|
||||||
|
await async_setup_compensation(hass, TEST_CONFIG)
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.test_state", 4)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get(TEST_SOURCE)
|
||||||
|
assert state.state == "4"
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
|
assert state.state == "5.0"
|
||||||
|
|
||||||
|
# Force Template Reload
|
||||||
|
yaml_path = get_fixture_path("sensor_configuration.yaml", "template")
|
||||||
|
with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
|
||||||
|
await hass.services.async_call(
|
||||||
|
"template",
|
||||||
|
SERVICE_RELOAD,
|
||||||
|
{},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Template state gets to None
|
||||||
|
state = hass.states.get(TEST_SOURCE)
|
||||||
|
assert state is None
|
||||||
|
|
||||||
|
# Filter sensor ignores None state setting state to STATE_UNKNOWN
|
||||||
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
Loading…
x
Reference in New Issue
Block a user