From 1bcf661f29fd1c4e0476d9abe86bf80967560a0c Mon Sep 17 00:00:00 2001 From: Petro Date: Sun, 11 Oct 2020 14:03:38 -0400 Subject: [PATCH 01/21] Add Compensation Integration Adds the Compensation Integration --- CODEOWNERS | 1 + .../components/compensation/__init__.py | 1 + .../components/compensation/const.py | 15 ++ .../components/compensation/manifest.json | 7 + .../components/compensation/sensor.py | 192 ++++++++++++++++++ tests/components/compensation/__init__.py | 1 + tests/components/compensation/test_sensor.py | 116 +++++++++++ 7 files changed, 333 insertions(+) create mode 100644 homeassistant/components/compensation/__init__.py create mode 100644 homeassistant/components/compensation/const.py create mode 100644 homeassistant/components/compensation/manifest.json create mode 100644 homeassistant/components/compensation/sensor.py create mode 100644 tests/components/compensation/__init__.py create mode 100644 tests/components/compensation/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index e3a7c5e73279c4..745c32d79825bc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -77,6 +77,7 @@ homeassistant/components/cisco_webex_teams/* @fbradyirl homeassistant/components/cloud/* @home-assistant/cloud homeassistant/components/cloudflare/* @ludeeus homeassistant/components/comfoconnect/* @michaelarnauts +homeassistant/components/compensation/* @Petro31 homeassistant/components/config/* @home-assistant/core homeassistant/components/configurator/* @home-assistant/core homeassistant/components/control4/* @lawtancool diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py new file mode 100644 index 00000000000000..ffa440fd47046f --- /dev/null +++ b/homeassistant/components/compensation/__init__.py @@ -0,0 +1 @@ +"""The Compensation integration.""" \ No newline at end of file diff --git a/homeassistant/components/compensation/const.py b/homeassistant/components/compensation/const.py new file mode 100644 index 00000000000000..72273ebd3e28e1 --- /dev/null +++ b/homeassistant/components/compensation/const.py @@ -0,0 +1,15 @@ +"""Compensation constants.""" + +DOMAIN = "compensation" +PLATFORM = "sensor" + +SENSOR = "compensation" + +CONF_DEGREE = "degree" +CONF_PRECISION = "precision" + +DEFAULT_NAME = "Compensation" +DEFAULT_DEGREE = 1 +DEFAULT_PRECISION = 2 + +MATCH_DATAPOINT = r"([-+]?[0-9]+\.?[0-9]*){1} -> ([-+]?[0-9]+\.?[0-9]*){1}" \ No newline at end of file diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json new file mode 100644 index 00000000000000..cbce80676698e3 --- /dev/null +++ b/homeassistant/components/compensation/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "compensation", + "name": "Compensation", + "documentation": "https://www.home-assistant.io/integrations/compensation", + "requirements": ["numpy==1.19.2"], + "codeowners": ["@Petro31"] +} diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py new file mode 100644 index 00000000000000..f64d76b3c28338 --- /dev/null +++ b/homeassistant/components/compensation/sensor.py @@ -0,0 +1,192 @@ +"""Support for compensation sensor.""" +import numpy as np +import re +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + ATTR_ENTITY_ID, + CONF_ENTITY_ID, + CONF_NAME, + CONF_ATTRIBUTE, + CONF_UNIT_OF_MEASUREMENT, + STATE_UNKNOWN, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_state_change_event + +from .const import ( + CONF_DEGREE, + CONF_PRECISION, + MATCH_DATAPOINT, + DEFAULT_DEGREE, + DEFAULT_PRECISION, + DEFAULT_NAME, +) + +_LOGGER = logging.getLogger(__name__) + +ATTR_ATTRIBUTE = "attribute" +ATTR_COEFFICIENTS = "coefficients" + +CONF_DATAPOINTS = "data_points" + + +def datapoints_greater_than_degree(value: dict) -> dict: + """Validate data point list is greater than polynomial degrees""" + if not len(value[CONF_DATAPOINTS]) > value[CONF_DEGREE]: + raise vol.Invalid( + f"{CONF_DATAPOINTS} must have at least {value[CONF_DEGREE]+1} {CONF_DATAPOINTS}" + ) + + return value + + +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_DATAPOINTS): vol.All( + cv.ensure_list(cv.matches_regex(MATCH_DATAPOINT)), + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ATTRIBUTE): cv.string, + vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int, + vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All( + vol.Coerce(int), + vol.Range(min=1, max=7), + ), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } + ), + datapoints_greater_than_degree, +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Compensation sensor.""" + + compensation = CompensationSensor( + hass, + config[CONF_ENTITY_ID], + config.get(CONF_NAME), + config.get(CONF_ATTRIBUTE), + config[CONF_PRECISION], + config[CONF_DEGREE], + config[CONF_DATAPOINTS], + config.get(CONF_UNIT_OF_MEASUREMENT), + ) + + async_add_entities([compensation], True) + + +class CompensationSensor(Entity): + """Representation of a Compensation sensor.""" + + def __init__( + self, + hass, + entity_id, + name, + attribute, + precision, + degree, + datapoints, + unit_of_measurement, + ): + """Initialization of the Compensation sensor.""" + self._entity_id = entity_id + self._name = name + self._precision = precision + self._attribute = attribute + self._unit_of_measurement = unit_of_measurement + + self._points = [] + for datapoint in datapoints: + match = re.match(MATCH_DATAPOINT, datapoint) + # we should always have x and y if the regex validation passed. + x_value, y_value = [float(v) for v in match.groups()] + self._points.append((x_value, y_value)) + + x_values, y_values = zip(*self._points) + self._coefficients = np.polyfit(x_values, y_values, degree) + self._poly = np.poly1d(self._coefficients) + + self._state = STATE_UNKNOWN + + @callback + def async_compensation_sensor_state_listener(event): + """Handle sensor state changes.""" + new_state = event.data.get("new_state") + if new_state is None: + return + + if self._unit_of_measurement is None and self._attribute is None: + self._unit_of_measurement = new_state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT + ) + + try: + if self._attribute: + value = float(new_state.attributes.get(self._attribute)) + else: + value = ( + None + if new_state.state == STATE_UNKNOWN + else float(new_state.state) + ) + # Calculate the result + self._state = round(self._poly(value), self._precision) + + except (ValueError, TypeError): + self._state = STATE_UNKNOWN + if self._attribute: + _LOGGER.warning( + "%s attribute %s is not numerical", + self._entity_id, + self._attribute, + ) + else: + _LOGGER.warning("%s state is not numerical", self._entity_id) + + self.async_write_ha_state() + + async_track_state_change_event( + hass, [entity_id], async_compensation_sensor_state_listener + ) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + ret = { + ATTR_ENTITY_ID: self._entity_id, + ATTR_COEFFICIENTS: self._coefficients.tolist(), + } + if self._attribute: + ret[ATTR_ATTRIBUTE] = self._attribute + return ret + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement diff --git a/tests/components/compensation/__init__.py b/tests/components/compensation/__init__.py new file mode 100644 index 00000000000000..55d365adc0e582 --- /dev/null +++ b/tests/components/compensation/__init__.py @@ -0,0 +1 @@ +"""Tests for the compensation component.""" diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py new file mode 100644 index 00000000000000..0e24767a58e2ea --- /dev/null +++ b/tests/components/compensation/test_sensor.py @@ -0,0 +1,116 @@ +"""The tests for the integration sensor platform.""" +from datetime import timedelta + +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT +from homeassistant.setup import async_setup_component +from homeassistant.components.compensation.sensor import ATTR_COEFFICIENTS + +from tests.async_mock import patch + + +async def test_linear_state(hass): + """Test compensation sensor state.""" + config = { + "sensor": { + "platform": "compensation", + "name": "compensation", + "entity_id": "sensor.uncompensated", + "data_points": [ + "1.0 -> 2.0", + "2.0 -> 3.0", + ], + "precision": 2, + ATTR_UNIT_OF_MEASUREMENT: "a", + } + } + + assert await async_setup_component(hass, "sensor", config) + + entity_id = config["sensor"]["entity_id"] + hass.states.async_set(entity_id, 4, {}) + await hass.async_block_till_done() + + state = hass.states.get("sensor.compensation") + assert state is not None + + assert round(float(state.state), config["sensor"]["precision"]) == 5.0 + + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == config["sensor"][ATTR_UNIT_OF_MEASUREMENT] + ) + + coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)] + assert coefs == [1.0, 1.0] + + +async def test_linear_state_from_attribute(hass): + """Test compensation sensor state that pulls from attribute.""" + config = { + "sensor": { + "platform": "compensation", + "name": "compensation", + "entity_id": "sensor.uncompensated", + "attribute": "value", + "data_points": [ + "1.0 -> 2.0", + "2.0 -> 3.0", + ], + "precision": 2, + } + } + + assert await async_setup_component(hass, "sensor", config) + + entity_id = config["sensor"]["entity_id"] + hass.states.async_set(entity_id, 3, {"value": 4}) + await hass.async_block_till_done() + + state = hass.states.get("sensor.compensation") + assert state is not None + + assert round(float(state.state), config["sensor"]["precision"]) == 5.0 + + coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)] + assert coefs == [1.0, 1.0] + + +async def test_quadratic_state(hass): + """Test 3 degree polynominial compensation sensor.""" + config = { + "sensor": { + "platform": "compensation", + "name": "compensation", + "entity_id": "sensor.temperature", + "data_points": [ + "50 -> 3.3", + "50 -> 2.8", + "50 -> 2.9", + "70 -> 2.3", + "70 -> 2.6", + "70 -> 2.1", + "80 -> 2.5", + "80 -> 2.9", + "80 -> 2.4", + "90 -> 3.0", + "90 -> 3.1", + "90 -> 2.8", + "100 -> 3.3", + "100 -> 3.5", + "100 -> 3.0", + ], + "degree": 2, + "precision": 3, + } + } + assert await async_setup_component(hass, "sensor", config) + + entity_id = config["sensor"]["entity_id"] + hass.states.async_set(entity_id, 43.2, {}) + await hass.async_block_till_done() + + state = hass.states.get("sensor.compensation") + + assert state is not None + + assert round(float(state.state), config["sensor"]["precision"]) == 3.327 \ No newline at end of file From 8066f5fbb5a4181f876b30c80af7b8812aa279a4 Mon Sep 17 00:00:00 2001 From: Petro Date: Sun, 11 Oct 2020 14:22:06 -0400 Subject: [PATCH 02/21] Add Requirements add missing requirements to compensation integration --- requirements_all.txt | 1 + requirements_test_all.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements_all.txt b/requirements_all.txt index 83225c396429d6..be321cb8b41308 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1003,6 +1003,7 @@ nuheat==0.3.0 # homeassistant.components.numato numato-gpio==0.8.0 +# homeassistant.components.compensation # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d17dffacbb4a46..efc51afca46b3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -480,6 +480,7 @@ nuheat==0.3.0 # homeassistant.components.numato numato-gpio==0.8.0 +# homeassistant.components.compensation # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow From 595d051a21ecf24fd70b81b78fde9a66b84fd70a Mon Sep 17 00:00:00 2001 From: Petro Date: Sun, 11 Oct 2020 14:30:45 -0400 Subject: [PATCH 03/21] Fix for tests Fix files after tests --- homeassistant/components/compensation/__init__.py | 2 +- homeassistant/components/compensation/const.py | 2 +- homeassistant/components/compensation/sensor.py | 4 ++-- tests/components/compensation/test_sensor.py | 8 ++------ 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index ffa440fd47046f..2c6edccd498d22 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -1 +1 @@ -"""The Compensation integration.""" \ No newline at end of file +"""The Compensation integration.""" diff --git a/homeassistant/components/compensation/const.py b/homeassistant/components/compensation/const.py index 72273ebd3e28e1..935bd3a5ba2d55 100644 --- a/homeassistant/components/compensation/const.py +++ b/homeassistant/components/compensation/const.py @@ -12,4 +12,4 @@ DEFAULT_DEGREE = 1 DEFAULT_PRECISION = 2 -MATCH_DATAPOINT = r"([-+]?[0-9]+\.?[0-9]*){1} -> ([-+]?[0-9]+\.?[0-9]*){1}" \ No newline at end of file +MATCH_DATAPOINT = r"([-+]?[0-9]+\.?[0-9]*){1} -> ([-+]?[0-9]+\.?[0-9]*){1}" diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index f64d76b3c28338..e76cd52a6cc698 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -38,7 +38,7 @@ def datapoints_greater_than_degree(value: dict) -> dict: - """Validate data point list is greater than polynomial degrees""" + """Validate data point list is greater than polynomial degrees.""" if not len(value[CONF_DATAPOINTS]) > value[CONF_DEGREE]: raise vol.Invalid( f"{CONF_DATAPOINTS} must have at least {value[CONF_DEGREE]+1} {CONF_DATAPOINTS}" @@ -99,7 +99,7 @@ def __init__( datapoints, unit_of_measurement, ): - """Initialization of the Compensation sensor.""" + """Initialize the Compensation sensor.""" self._entity_id = entity_id self._name = name self._precision = precision diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index 0e24767a58e2ea..72e698a7e38070 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -1,11 +1,7 @@ """The tests for the integration sensor platform.""" -from datetime import timedelta - +from homeassistant.components.compensation.sensor import ATTR_COEFFICIENTS from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.setup import async_setup_component -from homeassistant.components.compensation.sensor import ATTR_COEFFICIENTS - -from tests.async_mock import patch async def test_linear_state(hass): @@ -113,4 +109,4 @@ async def test_quadratic_state(hass): assert state is not None - assert round(float(state.state), config["sensor"]["precision"]) == 3.327 \ No newline at end of file + assert round(float(state.state), config["sensor"]["precision"]) == 3.327 From 30a955019cae60741852dcf391323cf59a17ae51 Mon Sep 17 00:00:00 2001 From: Petro Date: Sun, 11 Oct 2020 14:41:47 -0400 Subject: [PATCH 04/21] Fix isort ran isort --- homeassistant/components/compensation/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index e76cd52a6cc698..8010c78de3dd05 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -1,17 +1,17 @@ """Support for compensation sensor.""" -import numpy as np -import re import logging +import re +import numpy as np import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, + CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_NAME, - CONF_ATTRIBUTE, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, ) @@ -23,10 +23,10 @@ from .const import ( CONF_DEGREE, CONF_PRECISION, - MATCH_DATAPOINT, DEFAULT_DEGREE, - DEFAULT_PRECISION, DEFAULT_NAME, + DEFAULT_PRECISION, + MATCH_DATAPOINT, ) _LOGGER = logging.getLogger(__name__) From 65da99344971f751d6c8e40309dd94ad15ee6052 Mon Sep 17 00:00:00 2001 From: Petro Date: Mon, 12 Oct 2020 20:31:12 -0400 Subject: [PATCH 05/21] Handle ADR-0007 Change the configuration to deal with ADR-0007 --- .../components/compensation/__init__.py | 84 ++++++++++++ .../components/compensation/const.py | 6 +- .../components/compensation/sensor.py | 75 ++++------ tests/components/compensation/test_sensor.py | 129 ++++++++++-------- 4 files changed, 186 insertions(+), 108 deletions(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index 2c6edccd498d22..24ad299bf48cd8 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -1 +1,85 @@ """The Compensation integration.""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + CONF_ATTRIBUTE, + CONF_ENTITY_ID, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.discovery import async_load_platform + +from .const import ( + CONF_COMPENSATION, + CONF_DATAPOINTS, + CONF_DEGREE, + CONF_PRECISION, + DATA_COMPENSATION, + DEFAULT_DEGREE, + DEFAULT_NAME, + DEFAULT_PRECISION, + DOMAIN, + MATCH_DATAPOINT, +) + +_LOGGER = logging.getLogger(__name__) + + +def datapoints_greater_than_degree(value: dict) -> dict: + """Validate data point list is greater than polynomial degrees.""" + if not len(value[CONF_DATAPOINTS]) > value[CONF_DEGREE]: + raise vol.Invalid( + f"{CONF_DATAPOINTS} must have at least {value[CONF_DEGREE]+1} {CONF_DATAPOINTS}" + ) + + return value + + +COMPENSATION_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_DATAPOINTS): vol.All( + cv.ensure_list(cv.matches_regex(MATCH_DATAPOINT)), + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ATTRIBUTE): cv.string, + vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int, + vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All( + vol.Coerce(int), + vol.Range(min=1, max=7), + ), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {cv.slug: vol.All(COMPENSATION_SCHEMA, datapoints_greater_than_degree)} + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Compensation sensor.""" + + hass.data[DATA_COMPENSATION] = {} + + for compensation, conf in config.get(DOMAIN).items(): + _LOGGER.debug("Setup %s.%s", DOMAIN, compensation) + + hass.data[DATA_COMPENSATION][compensation] = conf + + hass.async_create_task( + async_load_platform( + hass, SENSOR_DOMAIN, DOMAIN, {CONF_COMPENSATION: compensation}, config + ) + ) + + return True diff --git a/homeassistant/components/compensation/const.py b/homeassistant/components/compensation/const.py index 935bd3a5ba2d55..29153056087eea 100644 --- a/homeassistant/components/compensation/const.py +++ b/homeassistant/components/compensation/const.py @@ -1,13 +1,15 @@ """Compensation constants.""" - DOMAIN = "compensation" -PLATFORM = "sensor" SENSOR = "compensation" +CONF_COMPENSATION = "compensation" +CONF_DATAPOINTS = "data_points" CONF_DEGREE = "degree" CONF_PRECISION = "precision" +DATA_COMPENSATION = "compensation_data" + DEFAULT_NAME = "Compensation" DEFAULT_DEGREE = 1 DEFAULT_PRECISION = 2 diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 8010c78de3dd05..12d0d91e148b2e 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -3,9 +3,7 @@ import re import numpy as np -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, @@ -16,16 +14,16 @@ STATE_UNKNOWN, ) from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change_event from .const import ( + CONF_COMPENSATION, + CONF_DATAPOINTS, CONF_DEGREE, CONF_PRECISION, - DEFAULT_DEGREE, - DEFAULT_NAME, - DEFAULT_PRECISION, + DATA_COMPENSATION, + DOMAIN, MATCH_DATAPOINT, ) @@ -34,56 +32,31 @@ ATTR_ATTRIBUTE = "attribute" ATTR_COEFFICIENTS = "coefficients" -CONF_DATAPOINTS = "data_points" - - -def datapoints_greater_than_degree(value: dict) -> dict: - """Validate data point list is greater than polynomial degrees.""" - if not len(value[CONF_DATAPOINTS]) > value[CONF_DEGREE]: - raise vol.Invalid( - f"{CONF_DATAPOINTS} must have at least {value[CONF_DEGREE]+1} {CONF_DATAPOINTS}" - ) - - return value - - -PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_DATAPOINTS): vol.All( - cv.ensure_list(cv.matches_regex(MATCH_DATAPOINT)), - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_ATTRIBUTE): cv.string, - vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int, - vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All( - vol.Coerce(int), - vol.Range(min=1, max=7), - ), - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - } - ), - datapoints_greater_than_degree, -) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Compensation sensor.""" - - compensation = CompensationSensor( - hass, - config[CONF_ENTITY_ID], - config.get(CONF_NAME), - config.get(CONF_ATTRIBUTE), - config[CONF_PRECISION], - config[CONF_DEGREE], - config[CONF_DATAPOINTS], - config.get(CONF_UNIT_OF_MEASUREMENT), + if discovery_info is None: + _LOGGER.error("This platform is only available through discovery") + return + + compensation = discovery_info.get(CONF_COMPENSATION) + conf = hass.data[DATA_COMPENSATION][compensation] + + async_add_entities( + [ + CompensationSensor( + hass, + conf[CONF_ENTITY_ID], + conf.get(CONF_NAME), + conf.get(CONF_ATTRIBUTE), + conf[CONF_PRECISION], + conf[CONF_DEGREE], + conf[CONF_DATAPOINTS], + conf.get(CONF_UNIT_OF_MEASUREMENT), + ) + ] ) - async_add_entities([compensation], True) - class CompensationSensor(Entity): """Representation of a Compensation sensor.""" diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index 72e698a7e38070..360c01988e1c1d 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -1,39 +1,48 @@ """The tests for the integration sensor platform.""" -from homeassistant.components.compensation.sensor import ATTR_COEFFICIENTS -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT +from homeassistant.components.compensation.sensor import ( + ATTR_COEFFICIENTS, + CONF_PRECISION, + DOMAIN, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component async def test_linear_state(hass): """Test compensation sensor state.""" config = { - "sensor": { - "platform": "compensation", - "name": "compensation", - "entity_id": "sensor.uncompensated", - "data_points": [ - "1.0 -> 2.0", - "2.0 -> 3.0", - ], - "precision": 2, - ATTR_UNIT_OF_MEASUREMENT: "a", + "compensation": { + "test": { + "name": "compensation", + "entity_id": "sensor.uncompensated", + "data_points": [ + "1.0 -> 2.0", + "2.0 -> 3.0", + ], + "precision": 2, + "unit_of_measurement": "a", + } } } - assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() - entity_id = config["sensor"]["entity_id"] + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id = config[DOMAIN]["test"]["entity_id"] hass.states.async_set(entity_id, 4, {}) await hass.async_block_till_done() state = hass.states.get("sensor.compensation") assert state is not None - assert round(float(state.state), config["sensor"]["precision"]) == 5.0 + assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 5.0 assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == config["sensor"][ATTR_UNIT_OF_MEASUREMENT] + == config[DOMAIN]["test"][ATTR_UNIT_OF_MEASUREMENT] ) coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)] @@ -43,29 +52,34 @@ async def test_linear_state(hass): async def test_linear_state_from_attribute(hass): """Test compensation sensor state that pulls from attribute.""" config = { - "sensor": { - "platform": "compensation", - "name": "compensation", - "entity_id": "sensor.uncompensated", - "attribute": "value", - "data_points": [ - "1.0 -> 2.0", - "2.0 -> 3.0", - ], - "precision": 2, + "compensation": { + "test": { + "name": "compensation", + "entity_id": "sensor.uncompensated", + "attribute": "value", + "data_points": [ + "1.0 -> 2.0", + "2.0 -> 3.0", + ], + "precision": 2, + } } } - assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - entity_id = config["sensor"]["entity_id"] + entity_id = config[DOMAIN]["test"]["entity_id"] hass.states.async_set(entity_id, 3, {"value": 4}) await hass.async_block_till_done() state = hass.states.get("sensor.compensation") assert state is not None - assert round(float(state.state), config["sensor"]["precision"]) == 5.0 + assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 5.0 coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)] assert coefs == [1.0, 1.0] @@ -74,34 +88,39 @@ async def test_linear_state_from_attribute(hass): async def test_quadratic_state(hass): """Test 3 degree polynominial compensation sensor.""" config = { - "sensor": { - "platform": "compensation", - "name": "compensation", - "entity_id": "sensor.temperature", - "data_points": [ - "50 -> 3.3", - "50 -> 2.8", - "50 -> 2.9", - "70 -> 2.3", - "70 -> 2.6", - "70 -> 2.1", - "80 -> 2.5", - "80 -> 2.9", - "80 -> 2.4", - "90 -> 3.0", - "90 -> 3.1", - "90 -> 2.8", - "100 -> 3.3", - "100 -> 3.5", - "100 -> 3.0", - ], - "degree": 2, - "precision": 3, + "compensation": { + "test": { + "name": "compensation", + "entity_id": "sensor.temperature", + "data_points": [ + "50 -> 3.3", + "50 -> 2.8", + "50 -> 2.9", + "70 -> 2.3", + "70 -> 2.6", + "70 -> 2.1", + "80 -> 2.5", + "80 -> 2.9", + "80 -> 2.4", + "90 -> 3.0", + "90 -> 3.1", + "90 -> 2.8", + "100 -> 3.3", + "100 -> 3.5", + "100 -> 3.0", + ], + "degree": 2, + "precision": 3, + } } } - assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - entity_id = config["sensor"]["entity_id"] + entity_id = config[DOMAIN]["test"]["entity_id"] hass.states.async_set(entity_id, 43.2, {}) await hass.async_block_till_done() @@ -109,4 +128,4 @@ async def test_quadratic_state(hass): assert state is not None - assert round(float(state.state), config["sensor"]["precision"]) == 3.327 + assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 3.327 From 2795e7dc780c6d7a461c268bb74f587a45aa6778 Mon Sep 17 00:00:00 2001 From: Petro Date: Mon, 12 Oct 2020 20:44:09 -0400 Subject: [PATCH 06/21] fix flake8 Fix flake8 --- homeassistant/components/compensation/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 12d0d91e148b2e..f863655b6570bc 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -23,7 +23,6 @@ CONF_DEGREE, CONF_PRECISION, DATA_COMPENSATION, - DOMAIN, MATCH_DATAPOINT, ) From 40e2cdbf7307e02643ce4ec12a73d75a32e1a156 Mon Sep 17 00:00:00 2001 From: Petro Date: Tue, 13 Oct 2020 21:18:03 -0400 Subject: [PATCH 07/21] Added Error Trapping Catch errors. Raise Rank Warnings but continue. Fixed bad imports --- .../components/compensation/__init__.py | 62 ++++++++++++++++--- .../components/compensation/const.py | 1 + .../components/compensation/sensor.py | 25 ++------ tests/components/compensation/test_sensor.py | 7 +-- 4 files changed, 64 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index 24ad299bf48cd8..73cd135cdc85ae 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -1,6 +1,10 @@ """The Compensation integration.""" import logging +import re +import warnings +import numpy as np +from numpy.linalg import LinAlgError import voluptuous as vol from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -17,6 +21,7 @@ CONF_COMPENSATION, CONF_DATAPOINTS, CONF_DEGREE, + CONF_POLYNOMIAL, CONF_PRECISION, DATA_COMPENSATION, DEFAULT_DEGREE, @@ -68,18 +73,61 @@ def datapoints_greater_than_degree(value: dict) -> dict: async def async_setup(hass, config): """Set up the Compensation sensor.""" - hass.data[DATA_COMPENSATION] = {} + np.seterr(all="raise") + for compensation, conf in config.get(DOMAIN).items(): _LOGGER.debug("Setup %s.%s", DOMAIN, compensation) - hass.data[DATA_COMPENSATION][compensation] = conf - - hass.async_create_task( - async_load_platform( - hass, SENSOR_DOMAIN, DOMAIN, {CONF_COMPENSATION: compensation}, config + degree = conf[CONF_DEGREE] + + datapoints = [] + for datapoint in conf[CONF_DATAPOINTS]: + match = re.match(MATCH_DATAPOINT, datapoint) + # we should always have x and y if the regex validation passed. + x_value, y_value = [float(v) for v in match.groups()] + datapoints.append((x_value, y_value)) + + # get x values and y values from the x,y point pairs + x_values, y_values = zip(*datapoints) + + # try to get valid coefficients for a polynomial + coefficients = None + with warnings.catch_warnings(record=True) as all_warnings: + warnings.simplefilter("always") + # try to catch 3 possible errors + try: + coefficients = np.polyfit(x_values, y_values, degree) + except (ValueError, LinAlgError, FloatingPointError) as e: + _LOGGER.error( + "Setup of %s.%s encountered an error, %s.", DOMAIN, compensation, e + ) + # raise any warnings + for warning in all_warnings: + _LOGGER.warning( + "Setup of %s.%s encountered a warning, %s", + DOMAIN, + compensation, + warning.message.lower(), + ) + + 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) + + hass.data[DATA_COMPENSATION][compensation] = data + + hass.async_create_task( + async_load_platform( + hass, + SENSOR_DOMAIN, + DOMAIN, + {CONF_COMPENSATION: compensation}, + config, + ) ) - ) return True diff --git a/homeassistant/components/compensation/const.py b/homeassistant/components/compensation/const.py index 29153056087eea..bc391b89281270 100644 --- a/homeassistant/components/compensation/const.py +++ b/homeassistant/components/compensation/const.py @@ -7,6 +7,7 @@ CONF_DATAPOINTS = "data_points" CONF_DEGREE = "degree" CONF_PRECISION = "precision" +CONF_POLYNOMIAL = "polynomial" DATA_COMPENSATION = "compensation_data" diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index f863655b6570bc..51fc6b14f52b2f 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -19,8 +19,7 @@ from .const import ( CONF_COMPENSATION, - CONF_DATAPOINTS, - CONF_DEGREE, + CONF_POLYNOMIAL, CONF_PRECISION, DATA_COMPENSATION, MATCH_DATAPOINT, @@ -49,8 +48,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= conf.get(CONF_NAME), conf.get(CONF_ATTRIBUTE), conf[CONF_PRECISION], - conf[CONF_DEGREE], - conf[CONF_DATAPOINTS], + conf[CONF_POLYNOMIAL], conf.get(CONF_UNIT_OF_MEASUREMENT), ) ] @@ -67,8 +65,7 @@ def __init__( name, attribute, precision, - degree, - datapoints, + polynomial, unit_of_measurement, ): """Initialize the Compensation sensor.""" @@ -77,18 +74,8 @@ def __init__( self._precision = precision self._attribute = attribute self._unit_of_measurement = unit_of_measurement - - self._points = [] - for datapoint in datapoints: - match = re.match(MATCH_DATAPOINT, datapoint) - # we should always have x and y if the regex validation passed. - x_value, y_value = [float(v) for v in match.groups()] - self._points.append((x_value, y_value)) - - x_values, y_values = zip(*self._points) - self._coefficients = np.polyfit(x_values, y_values, degree) - self._poly = np.poly1d(self._coefficients) - + self._poly = polynomial + self._coefficients = polynomial.coefficients.tolist() self._state = STATE_UNKNOWN @callback @@ -152,7 +139,7 @@ def device_state_attributes(self): """Return the state attributes of the sensor.""" ret = { ATTR_ENTITY_ID: self._entity_id, - ATTR_COEFFICIENTS: self._coefficients.tolist(), + ATTR_COEFFICIENTS: self._coefficients, } if self._attribute: ret[ATTR_ATTRIBUTE] = self._attribute diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index 360c01988e1c1d..46967780a7306e 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -1,9 +1,6 @@ """The tests for the integration sensor platform.""" -from homeassistant.components.compensation.sensor import ( - ATTR_COEFFICIENTS, - CONF_PRECISION, - DOMAIN, -) +from homeassistant.components.compensation.const import CONF_PRECISION, DOMAIN +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, EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component From 60c17b65c6682859d8bdf7242430b8a260ad9473 Mon Sep 17 00:00:00 2001 From: Petro Date: Tue, 13 Oct 2020 21:49:23 -0400 Subject: [PATCH 08/21] fix flake8 & pylint --- homeassistant/components/compensation/__init__.py | 12 +++++++----- homeassistant/components/compensation/sensor.py | 4 ---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index 73cd135cdc85ae..c9eb5bdd5b44a2 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -4,7 +4,6 @@ import warnings import numpy as np -from numpy.linalg import LinAlgError import voluptuous as vol from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -99,17 +98,20 @@ async def async_setup(hass, config): # try to catch 3 possible errors try: coefficients = np.polyfit(x_values, y_values, degree) - except (ValueError, LinAlgError, FloatingPointError) as e: + except FloatingPointError as error: _LOGGER.error( - "Setup of %s.%s encountered an error, %s.", DOMAIN, compensation, e + "Setup of %s.%s encountered an error, %s.", + DOMAIN, + compensation, + error, ) # raise any warnings for warning in all_warnings: _LOGGER.warning( - "Setup of %s.%s encountered a warning, %s", + "Setup of %s.%s encountered a warning, %s.", DOMAIN, compensation, - warning.message.lower(), + str(warning.message).lower(), ) if coefficients is not None: diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 51fc6b14f52b2f..e6b9e69843eed4 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -1,8 +1,5 @@ """Support for compensation sensor.""" import logging -import re - -import numpy as np from homeassistant.const import ( ATTR_ENTITY_ID, @@ -22,7 +19,6 @@ CONF_POLYNOMIAL, CONF_PRECISION, DATA_COMPENSATION, - MATCH_DATAPOINT, ) _LOGGER = logging.getLogger(__name__) From 81ad51eeaa0a12cb8bf34bd4db0700354f1bddc1 Mon Sep 17 00:00:00 2001 From: Petro Date: Tue, 13 Oct 2020 21:58:29 -0400 Subject: [PATCH 09/21] fix isort.... again --- homeassistant/components/compensation/sensor.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index e6b9e69843eed4..439e675c67ec47 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -14,12 +14,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change_event -from .const import ( - CONF_COMPENSATION, - CONF_POLYNOMIAL, - CONF_PRECISION, - DATA_COMPENSATION, -) +from .const import CONF_COMPENSATION, CONF_POLYNOMIAL, CONF_PRECISION, DATA_COMPENSATION _LOGGER = logging.getLogger(__name__) From 03003048ef7403ed449b05b8adb05324552bbbeb Mon Sep 17 00:00:00 2001 From: Petro Date: Thu, 15 Oct 2020 14:13:08 -0400 Subject: [PATCH 10/21] fix tests & comments fix tests and comments --- .../components/compensation/__init__.py | 41 +++++---- tests/components/compensation/test_sensor.py | 85 ++++++++++++++++++- 2 files changed, 102 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index c9eb5bdd5b44a2..dbda4ea9ed5a87 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -74,8 +74,6 @@ async def async_setup(hass, config): """Set up the Compensation sensor.""" hass.data[DATA_COMPENSATION] = {} - np.seterr(all="raise") - for compensation, conf in config.get(DOMAIN).items(): _LOGGER.debug("Setup %s.%s", DOMAIN, compensation) @@ -93,26 +91,25 @@ async def async_setup(hass, config): # try to get valid coefficients for a polynomial coefficients = None - with warnings.catch_warnings(record=True) as all_warnings: - warnings.simplefilter("always") - # try to catch 3 possible errors - try: - coefficients = np.polyfit(x_values, y_values, degree) - except FloatingPointError as error: - _LOGGER.error( - "Setup of %s.%s encountered an error, %s.", - DOMAIN, - compensation, - error, - ) - # raise any warnings - for warning in all_warnings: - _LOGGER.warning( - "Setup of %s.%s encountered a warning, %s.", - DOMAIN, - compensation, - str(warning.message).lower(), - ) + with np.errstate(all="raise"): + with warnings.catch_warnings(record=True) as all_warnings: + warnings.simplefilter("always") + # try to catch 3 possible errors + try: + coefficients = np.polyfit(x_values, y_values, degree) + except FloatingPointError as error: + _LOGGER.error( + "Setup of %s encountered an error, %s.", + compensation, + error, + ) + # raise any warnings + for warning in all_warnings: + _LOGGER.warning( + "Setup of %s encountered a warning, %s.", + compensation, + str(warning.message).lower(), + ) if coefficients is not None: data = { diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index 46967780a7306e..67109ff33d17b7 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -1,9 +1,19 @@ """The tests for the integration sensor platform.""" +import unittest + +import pytest + from homeassistant.components.compensation.const import CONF_PRECISION, DOMAIN 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, EVENT_HOMEASSISTANT_START -from homeassistant.setup import async_setup_component +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + EVENT_HOMEASSISTANT_START, + STATE_UNKNOWN, +) +from homeassistant.setup import async_prepare_setup_platform, async_setup_component + +from tests.common import logging async def test_linear_state(hass): @@ -45,6 +55,14 @@ async def test_linear_state(hass): 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("sensor.compensation") + assert state is not None + + assert state.state == STATE_UNKNOWN + async def test_linear_state_from_attribute(hass): """Test compensation sensor state that pulls from attribute.""" @@ -81,6 +99,14 @@ async def test_linear_state_from_attribute(hass): coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)] assert coefs == [1.0, 1.0] + hass.states.async_set(entity_id, 3, {"value": "bar"}) + await hass.async_block_till_done() + + state = hass.states.get("sensor.compensation") + assert state is not None + + assert state.state == STATE_UNKNOWN + async def test_quadratic_state(hass): """Test 3 degree polynominial compensation sensor.""" @@ -126,3 +152,58 @@ async def test_quadratic_state(hass): assert state is not None assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 3.327 + + +async def test_numpy_errors(hass, caplog): + """Tests bad data points.""" + config = { + "compensation": { + "test": { + "name": "compensation", + "entity_id": "sensor.uncompensated", + "data_points": [ + "1.0 -> 1.0", + "1.0 -> 1.0", + ], + }, + "test2": { + "name": "compensation2", + "entity_id": "sensor.uncompensated", + "data_points": [ + "0.0 -> 1.0", + "0.0 -> 1.0", + ], + }, + } + } + await async_setup_component(hass, DOMAIN, config) + await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + + assert "polyfit may be poorly conditioned" in caplog.text + + assert "invalid value encountered in true_divide" in caplog.text + + +async def test_datapoints_greater_than_degree(hass, caplog): + """Tests bad data points.""" + config = { + "compensation": { + "test": { + "name": "compensation", + "entity_id": "sensor.uncompensated", + "data_points": [ + "1.0 -> 2.0", + "2.0 -> 3.0", + ], + "degree": 2, + }, + } + } + await async_setup_component(hass, DOMAIN, config) + await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + + assert "data_points must have at least 3 data_points" in caplog.text From 82e9ee6957fefe89c87d30c4393deaac42bf924e Mon Sep 17 00:00:00 2001 From: Petro Date: Thu, 15 Oct 2020 14:32:27 -0400 Subject: [PATCH 11/21] fix flake8 --- tests/components/compensation/test_sensor.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index 67109ff33d17b7..4ea49a5f3407bb 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -1,7 +1,4 @@ """The tests for the integration sensor platform.""" -import unittest - -import pytest from homeassistant.components.compensation.const import CONF_PRECISION, DOMAIN from homeassistant.components.compensation.sensor import ATTR_COEFFICIENTS @@ -11,9 +8,7 @@ EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, ) -from homeassistant.setup import async_prepare_setup_platform, async_setup_component - -from tests.common import logging +from homeassistant.setup import async_setup_component async def test_linear_state(hass): From 65064bca00665dea75c71ccbb2c06dc225503c48 Mon Sep 17 00:00:00 2001 From: Petro Date: Thu, 15 Oct 2020 15:03:28 -0400 Subject: [PATCH 12/21] remove discovery message --- .../components/compensation/sensor.py | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 439e675c67ec47..e856e9f7af1a3b 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -24,26 +24,23 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Compensation sensor.""" - if discovery_info is None: - _LOGGER.error("This platform is only available through discovery") - return - - compensation = discovery_info.get(CONF_COMPENSATION) - conf = hass.data[DATA_COMPENSATION][compensation] - - async_add_entities( - [ - CompensationSensor( - hass, - conf[CONF_ENTITY_ID], - conf.get(CONF_NAME), - conf.get(CONF_ATTRIBUTE), - conf[CONF_PRECISION], - conf[CONF_POLYNOMIAL], - conf.get(CONF_UNIT_OF_MEASUREMENT), - ) - ] - ) + if discovery_info is not None: + compensation = discovery_info.get(CONF_COMPENSATION) + conf = hass.data[DATA_COMPENSATION][compensation] + + async_add_entities( + [ + CompensationSensor( + hass, + conf[CONF_ENTITY_ID], + conf.get(CONF_NAME), + conf.get(CONF_ATTRIBUTE), + conf[CONF_PRECISION], + conf[CONF_POLYNOMIAL], + conf.get(CONF_UNIT_OF_MEASUREMENT), + ) + ] + ) class CompensationSensor(Entity): From bb57140c165efe04c329c485f7d5367e7354dc55 Mon Sep 17 00:00:00 2001 From: Petro Date: Sat, 27 Feb 2021 15:51:46 -0500 Subject: [PATCH 13/21] Fixed Review changes * Fixed review requests. * Added test to test get more coverage. --- .../components/compensation/__init__.py | 18 +-- .../components/compensation/manifest.json | 2 +- .../components/compensation/sensor.py | 118 +++++++++--------- tests/components/compensation/test_sensor.py | 80 ++++++++---- 4 files changed, 123 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index dbda4ea9ed5a87..611c539ab02af2 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -27,7 +27,6 @@ DEFAULT_NAME, DEFAULT_PRECISION, DOMAIN, - MATCH_DATAPOINT, ) _LOGGER = logging.getLogger(__name__) @@ -35,7 +34,7 @@ def datapoints_greater_than_degree(value: dict) -> dict: """Validate data point list is greater than polynomial degrees.""" - if not len(value[CONF_DATAPOINTS]) > value[CONF_DEGREE]: + if len(value[CONF_DATAPOINTS]) <= value[CONF_DEGREE]: raise vol.Invalid( f"{CONF_DATAPOINTS} must have at least {value[CONF_DEGREE]+1} {CONF_DATAPOINTS}" ) @@ -46,9 +45,9 @@ def datapoints_greater_than_degree(value: dict) -> dict: COMPENSATION_SCHEMA = vol.Schema( { vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_DATAPOINTS): vol.All( - cv.ensure_list(cv.matches_regex(MATCH_DATAPOINT)), - ), + vol.Required(CONF_DATAPOINTS): [ + vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)]) + ], vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_ATTRIBUTE): cv.string, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int, @@ -79,15 +78,8 @@ async def async_setup(hass, config): degree = conf[CONF_DEGREE] - datapoints = [] - for datapoint in conf[CONF_DATAPOINTS]: - match = re.match(MATCH_DATAPOINT, datapoint) - # we should always have x and y if the regex validation passed. - x_value, y_value = [float(v) for v in match.groups()] - datapoints.append((x_value, y_value)) - # get x values and y values from the x,y point pairs - x_values, y_values = zip(*datapoints) + x_values, y_values = zip(*conf[CONF_DATAPOINTS]) # try to get valid coefficients for a polynomial coefficients = None diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index cbce80676698e3..f9275225048e7d 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,6 +2,6 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.19.2"], + "requirements": ["numpy==1.20.1"], "codeowners": ["@Petro31"] } diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index e856e9f7af1a3b..5462d0b6be3a09 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -24,23 +24,25 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Compensation sensor.""" - if discovery_info is not None: - compensation = discovery_info.get(CONF_COMPENSATION) - conf = hass.data[DATA_COMPENSATION][compensation] - - async_add_entities( - [ - CompensationSensor( - hass, - conf[CONF_ENTITY_ID], - conf.get(CONF_NAME), - conf.get(CONF_ATTRIBUTE), - conf[CONF_PRECISION], - conf[CONF_POLYNOMIAL], - conf.get(CONF_UNIT_OF_MEASUREMENT), - ) - ] - ) + if discovery_info is None: + return + + compensation = discovery_info.get(CONF_COMPENSATION) + conf = hass.data[DATA_COMPENSATION][compensation] + + async_add_entities( + [ + CompensationSensor( + hass, + conf[CONF_ENTITY_ID], + conf.get(CONF_NAME), + conf.get(CONF_ATTRIBUTE), + conf[CONF_PRECISION], + conf[CONF_POLYNOMIAL], + conf.get(CONF_UNIT_OF_MEASUREMENT), + ) + ] + ) class CompensationSensor(Entity): @@ -66,45 +68,14 @@ def __init__( self._coefficients = polynomial.coefficients.tolist() self._state = STATE_UNKNOWN - @callback - def async_compensation_sensor_state_listener(event): - """Handle sensor state changes.""" - new_state = event.data.get("new_state") - if new_state is None: - return - - if self._unit_of_measurement is None and self._attribute is None: - self._unit_of_measurement = new_state.attributes.get( - ATTR_UNIT_OF_MEASUREMENT - ) - - try: - if self._attribute: - value = float(new_state.attributes.get(self._attribute)) - else: - value = ( - None - if new_state.state == STATE_UNKNOWN - else float(new_state.state) - ) - # Calculate the result - self._state = round(self._poly(value), self._precision) - - except (ValueError, TypeError): - self._state = STATE_UNKNOWN - if self._attribute: - _LOGGER.warning( - "%s attribute %s is not numerical", - self._entity_id, - self._attribute, - ) - else: - _LOGGER.warning("%s state is not numerical", self._entity_id) - - self.async_write_ha_state() - - async_track_state_change_event( - hass, [entity_id], async_compensation_sensor_state_listener + async def async_added_to_hass(self): + """Handle added to Hass.""" + self.async_on_remove( + async_track_state_change_event( + self.hass, + [self._entity_id], + self._async_compensation_sensor_state_listener, + ) ) @property @@ -137,3 +108,38 @@ def device_state_attributes(self): def unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement + + @callback + def _async_compensation_sensor_state_listener(self, event): + """Handle sensor state changes.""" + new_state = event.data.get("new_state") + if new_state is None: + return + + if self._unit_of_measurement is None and self._attribute is None: + self._unit_of_measurement = new_state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT + ) + + try: + if self._attribute: + value = float(new_state.attributes.get(self._attribute)) + else: + value = ( + None if new_state.state == STATE_UNKNOWN else float(new_state.state) + ) + # Calculate the result + self._state = round(self._poly(value), self._precision) + + except (ValueError, TypeError): + self._state = STATE_UNKNOWN + if self._attribute: + _LOGGER.warning( + "%s attribute %s is not numerical", + self._entity_id, + self._attribute, + ) + else: + _LOGGER.warning("%s state is not numerical", self._entity_id) + + self.async_write_ha_state() diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index 4ea49a5f3407bb..30ed85fa537625 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -6,6 +6,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, EVENT_HOMEASSISTANT_START, + EVENT_STATE_CHANGED, STATE_UNKNOWN, ) from homeassistant.setup import async_setup_component @@ -19,8 +20,8 @@ async def test_linear_state(hass): "name": "compensation", "entity_id": "sensor.uncompensated", "data_points": [ - "1.0 -> 2.0", - "2.0 -> 3.0", + [1.0, 2.0], + [2.0, 3.0], ], "precision": 2, "unit_of_measurement": "a", @@ -68,8 +69,8 @@ async def test_linear_state_from_attribute(hass): "entity_id": "sensor.uncompensated", "attribute": "value", "data_points": [ - "1.0 -> 2.0", - "2.0 -> 3.0", + [1.0, 2.0], + [2.0, 3.0], ], "precision": 2, } @@ -111,21 +112,21 @@ async def test_quadratic_state(hass): "name": "compensation", "entity_id": "sensor.temperature", "data_points": [ - "50 -> 3.3", - "50 -> 2.8", - "50 -> 2.9", - "70 -> 2.3", - "70 -> 2.6", - "70 -> 2.1", - "80 -> 2.5", - "80 -> 2.9", - "80 -> 2.4", - "90 -> 3.0", - "90 -> 3.1", - "90 -> 2.8", - "100 -> 3.3", - "100 -> 3.5", - "100 -> 3.0", + [50, 3.3], + [50, 2.8], + [50, 2.9], + [70, 2.3], + [70, 2.6], + [70, 2.1], + [80, 2.5], + [80, 2.9], + [80, 2.4], + [90, 3.0], + [90, 3.1], + [90, 2.8], + [100, 3.3], + [100, 3.5], + [100, 3.0], ], "degree": 2, "precision": 3, @@ -157,16 +158,16 @@ async def test_numpy_errors(hass, caplog): "name": "compensation", "entity_id": "sensor.uncompensated", "data_points": [ - "1.0 -> 1.0", - "1.0 -> 1.0", + [1.0, 1.0], + [1.0, 1.0], ], }, "test2": { "name": "compensation2", "entity_id": "sensor.uncompensated", "data_points": [ - "0.0 -> 1.0", - "0.0 -> 1.0", + [0.0, 1.0], + [0.0, 1.0], ], }, } @@ -189,8 +190,8 @@ async def test_datapoints_greater_than_degree(hass, caplog): "name": "compensation", "entity_id": "sensor.uncompensated", "data_points": [ - "1.0 -> 2.0", - "2.0 -> 3.0", + [1.0, 2.0], + [2.0, 3.0], ], "degree": 2, }, @@ -202,3 +203,32 @@ async def test_datapoints_greater_than_degree(hass, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) assert "data_points must have at least 3 data_points" in caplog.text + + +async def test_new_state_is_none(hass): + """Test compensation sensor state.""" + config = { + "compensation": { + "test": { + "name": "compensation", + "entity_id": "sensor.uncompensated", + "data_points": [ + [1.0, 2.0], + [2.0, 3.0], + ], + "precision": 2, + "unit_of_measurement": "a", + } + } + } + + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + last_changed = hass.states.get("sensor.compensation").last_changed + + hass.bus.async_fire( + EVENT_STATE_CHANGED, event_data={"entity_id": "sensor.uncompensated"} + ) + + assert last_changed == hass.states.get("sensor.compensation").last_changed From 182eb81a10e9e9ceb80b418f52f8ecd983fef3ac Mon Sep 17 00:00:00 2001 From: Petro Date: Sat, 27 Feb 2021 16:18:29 -0500 Subject: [PATCH 14/21] Roll back numpy requirement Roll back numpy requirement to match other integrations. --- homeassistant/components/compensation/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index f9275225048e7d..cbce80676698e3 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,6 +2,6 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.20.1"], + "requirements": ["numpy==1.19.2"], "codeowners": ["@Petro31"] } From 98b4eac5a4cc232f2a3be34f529de72114183b2d Mon Sep 17 00:00:00 2001 From: Petro Date: Sat, 27 Feb 2021 16:42:16 -0500 Subject: [PATCH 15/21] Fix flake8 --- homeassistant/components/compensation/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index 611c539ab02af2..c56c1c54fbfa6f 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -1,6 +1,5 @@ """The Compensation integration.""" import logging -import re import warnings import numpy as np From b666a78502d2c679a0af120a7af6d428ef97b1f5 Mon Sep 17 00:00:00 2001 From: Petro Date: Sun, 14 Mar 2021 12:53:05 -0400 Subject: [PATCH 16/21] Fix requested changes Removed some necessary comments. Changed a test case to be more readable. --- homeassistant/components/compensation/__init__.py | 6 +++--- homeassistant/components/compensation/sensor.py | 1 - tests/components/compensation/test_sensor.py | 5 +---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index c56c1c54fbfa6f..6155f0c4cdf1ad 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -85,7 +85,6 @@ async def async_setup(hass, config): with np.errstate(all="raise"): with warnings.catch_warnings(record=True) as all_warnings: warnings.simplefilter("always") - # try to catch 3 possible errors try: coefficients = np.polyfit(x_values, y_values, degree) except FloatingPointError as error: @@ -94,7 +93,6 @@ async def async_setup(hass, config): compensation, error, ) - # raise any warnings for warning in all_warnings: _LOGGER.warning( "Setup of %s encountered a warning, %s.", @@ -102,7 +100,9 @@ async def async_setup(hass, config): str(warning.message).lower(), ) - if coefficients is not None: + if coefficients is None: + continue + else: data = { k: v for k, v in conf.items() if k not in [CONF_DEGREE, CONF_DATAPOINTS] } diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 5462d0b6be3a09..4d39c312cf0c6f 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -128,7 +128,6 @@ def _async_compensation_sensor_state_listener(self, event): value = ( None if new_state.state == STATE_UNKNOWN else float(new_state.state) ) - # Calculate the result self._state = round(self._poly(value), self._precision) except (ValueError, TypeError): diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index 30ed85fa537625..d8a907e4f79193 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -43,10 +43,7 @@ async def test_linear_state(hass): assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 5.0 - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == config[DOMAIN]["test"][ATTR_UNIT_OF_MEASUREMENT] - ) + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "a" coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)] assert coefs == [1.0, 1.0] From ad7969326337e4fef9879f1e5be52a373b11652f Mon Sep 17 00:00:00 2001 From: Petro Date: Sat, 20 Mar 2021 05:45:58 -0400 Subject: [PATCH 17/21] Fix doc strings and continue * Fixed a few test case doc strings * Removed a continue/else --- homeassistant/components/compensation/__init__.py | 4 +--- tests/components/compensation/test_sensor.py | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index 6155f0c4cdf1ad..0603b200d8438b 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -100,9 +100,7 @@ async def async_setup(hass, config): str(warning.message).lower(), ) - if coefficients is None: - continue - else: + if coefficients is not None: data = { k: v for k, v in conf.items() if k not in [CONF_DEGREE, CONF_DATAPOINTS] } diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index d8a907e4f79193..c11f384c356b85 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -148,7 +148,7 @@ async def test_quadratic_state(hass): async def test_numpy_errors(hass, caplog): - """Tests bad data points.""" + """Tests bad polyfits.""" config = { "compensation": { "test": { @@ -180,7 +180,7 @@ async def test_numpy_errors(hass, caplog): async def test_datapoints_greater_than_degree(hass, caplog): - """Tests bad data points.""" + """Tests 3 bad data points.""" config = { "compensation": { "test": { @@ -203,7 +203,7 @@ async def test_datapoints_greater_than_degree(hass, caplog): async def test_new_state_is_none(hass): - """Test compensation sensor state.""" + """Tests catch for empty new states.""" config = { "compensation": { "test": { From aaee774512aef5003db433a7bbfb0dee843ce42b Mon Sep 17 00:00:00 2001 From: Petro Date: Sat, 20 Mar 2021 06:07:26 -0400 Subject: [PATCH 18/21] Remove periods from logger Removed periods from _LOGGER errors. --- homeassistant/components/compensation/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index 0603b200d8438b..9b2aa787d4738c 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -89,13 +89,13 @@ async def async_setup(hass, config): coefficients = np.polyfit(x_values, y_values, degree) except FloatingPointError as error: _LOGGER.error( - "Setup of %s encountered an error, %s.", + "Setup of %s encountered an error, %s", compensation, error, ) for warning in all_warnings: _LOGGER.warning( - "Setup of %s encountered a warning, %s.", + "Setup of %s encountered a warning, %s", compensation, str(warning.message).lower(), ) From 857543677fc9f43c45fc2b754d5e5249366be55d Mon Sep 17 00:00:00 2001 From: Petro Date: Thu, 1 Apr 2021 21:28:52 -0400 Subject: [PATCH 19/21] Fixes changed name to unqiue_id. implemented suggested changes. --- .../components/compensation/__init__.py | 17 +++-- .../components/compensation/const.py | 3 - .../components/compensation/manifest.json | 2 +- .../components/compensation/sensor.py | 64 +++++++++---------- tests/components/compensation/test_sensor.py | 61 +++++++++--------- 5 files changed, 73 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index 9b2aa787d4738c..31bcf30760c1fe 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -8,8 +8,8 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( CONF_ATTRIBUTE, - CONF_ENTITY_ID, - CONF_NAME, + CONF_SOURCE, + CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, ) from homeassistant.helpers import config_validation as cv @@ -23,7 +23,6 @@ CONF_PRECISION, DATA_COMPENSATION, DEFAULT_DEGREE, - DEFAULT_NAME, DEFAULT_PRECISION, DOMAIN, ) @@ -43,11 +42,11 @@ def datapoints_greater_than_degree(value: dict) -> dict: COMPENSATION_SCHEMA = vol.Schema( { - vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_SOURCE): cv.entity_id, vol.Required(CONF_DATAPOINTS): [ vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)]) ], - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_ATTRIBUTE): cv.string, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int, vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All( @@ -106,6 +105,14 @@ async def async_setup(hass, config): } data[CONF_POLYNOMIAL] = np.poly1d(coefficients) + unique_id = data.get(CONF_UNIQUE_ID) + if unique_id is None: + identifier = data[CONF_SOURCE] + attribute = data.get(CONF_ATTRIBUTE) + data[CONF_UNIQUE_ID] = ( + identifier if attribute is None else f"{identifier}.{attribute}" + ) + hass.data[DATA_COMPENSATION][compensation] = data hass.async_create_task( diff --git a/homeassistant/components/compensation/const.py b/homeassistant/components/compensation/const.py index bc391b89281270..79133dd36741ae 100644 --- a/homeassistant/components/compensation/const.py +++ b/homeassistant/components/compensation/const.py @@ -11,8 +11,5 @@ DATA_COMPENSATION = "compensation_data" -DEFAULT_NAME = "Compensation" DEFAULT_DEGREE = 1 DEFAULT_PRECISION = 2 - -MATCH_DATAPOINT = r"([-+]?[0-9]+\.?[0-9]*){1} -> ([-+]?[0-9]+\.?[0-9]*){1}" diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index cbce80676698e3..86efbce72c87bd 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,6 +2,6 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.19.2"], + "requirements": ["numpy==1.20.2"], "codeowners": ["@Petro31"] } diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 4d39c312cf0c6f..3163ec3d70c78c 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -1,25 +1,25 @@ """Support for compensation sensor.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ATTRIBUTE, - CONF_ENTITY_ID, - CONF_NAME, + CONF_SOURCE, + CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, ) from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change_event from .const import CONF_COMPENSATION, CONF_POLYNOMIAL, CONF_PRECISION, DATA_COMPENSATION _LOGGER = logging.getLogger(__name__) -ATTR_ATTRIBUTE = "attribute" ATTR_COEFFICIENTS = "coefficients" +ATTR_SOURCE = "source" +ATTR_SOURCE_ATTRIBUTE = "source_attribute" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -27,15 +27,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if discovery_info is None: return - compensation = discovery_info.get(CONF_COMPENSATION) + compensation = discovery_info[CONF_COMPENSATION] conf = hass.data[DATA_COMPENSATION][compensation] async_add_entities( [ CompensationSensor( - hass, - conf[CONF_ENTITY_ID], - conf.get(CONF_NAME), + conf[CONF_SOURCE], + conf.get(CONF_UNIQUE_ID), conf.get(CONF_ATTRIBUTE), conf[CONF_PRECISION], conf[CONF_POLYNOMIAL], @@ -45,43 +44,42 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class CompensationSensor(Entity): +class CompensationSensor(SensorEntity): """Representation of a Compensation sensor.""" def __init__( self, - hass, - entity_id, - name, + source, + unique_id, attribute, precision, polynomial, unit_of_measurement, ): """Initialize the Compensation sensor.""" - self._entity_id = entity_id - self._name = name + self._source_entity_id = source self._precision = precision - self._attribute = attribute + self._source_attribute = attribute self._unit_of_measurement = unit_of_measurement self._poly = polynomial self._coefficients = polynomial.coefficients.tolist() - self._state = STATE_UNKNOWN + self._state = None + self._unique_id = unique_id async def async_added_to_hass(self): """Handle added to Hass.""" self.async_on_remove( async_track_state_change_event( self.hass, - [self._entity_id], + [self._source_entity_id], self._async_compensation_sensor_state_listener, ) ) @property - def name(self): - """Return the name of the sensor.""" - return self._name + def unique_id(self): + """Return the unique id of this sensor.""" + return self._unique_id @property def should_poll(self): @@ -94,14 +92,14 @@ def state(self): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" ret = { - ATTR_ENTITY_ID: self._entity_id, + ATTR_SOURCE: self._source_entity_id, ATTR_COEFFICIENTS: self._coefficients, } - if self._attribute: - ret[ATTR_ATTRIBUTE] = self._attribute + if self._source_attribute: + ret[ATTR_SOURCE_ATTRIBUTE] = self._source_attribute return ret @property @@ -116,14 +114,14 @@ def _async_compensation_sensor_state_listener(self, event): if new_state is None: return - if self._unit_of_measurement is None and self._attribute is None: + if self._unit_of_measurement is None and self._source_attribute is None: self._unit_of_measurement = new_state.attributes.get( ATTR_UNIT_OF_MEASUREMENT ) try: - if self._attribute: - value = float(new_state.attributes.get(self._attribute)) + if self._source_attribute: + value = float(new_state.attributes.get(self._source_attribute)) else: value = ( None if new_state.state == STATE_UNKNOWN else float(new_state.state) @@ -131,14 +129,14 @@ def _async_compensation_sensor_state_listener(self, event): self._state = round(self._poly(value), self._precision) except (ValueError, TypeError): - self._state = STATE_UNKNOWN - if self._attribute: + self._state = None + if self._source_attribute: _LOGGER.warning( "%s attribute %s is not numerical", - self._entity_id, - self._attribute, + self._source_entity_id, + self._source_attribute, ) else: - _LOGGER.warning("%s state is not numerical", self._entity_id) + _LOGGER.warning("%s state is not numerical", self._source_entity_id) self.async_write_ha_state() diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index c11f384c356b85..3bd86280750ff7 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -17,8 +17,7 @@ async def test_linear_state(hass): config = { "compensation": { "test": { - "name": "compensation", - "entity_id": "sensor.uncompensated", + "source": "sensor.uncompensated", "data_points": [ [1.0, 2.0], [2.0, 3.0], @@ -28,17 +27,18 @@ async def test_linear_state(hass): } } } + expected_entity_id = "sensor.compensation_sensor_uncompensated" assert await async_setup_component(hass, DOMAIN, config) assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - entity_id = config[DOMAIN]["test"]["entity_id"] + entity_id = config[DOMAIN]["test"]["source"] hass.states.async_set(entity_id, 4, {}) await hass.async_block_till_done() - state = hass.states.get("sensor.compensation") + state = hass.states.get(expected_entity_id) assert state is not None assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 5.0 @@ -51,7 +51,7 @@ async def test_linear_state(hass): hass.states.async_set(entity_id, "foo", {}) await hass.async_block_till_done() - state = hass.states.get("sensor.compensation") + state = hass.states.get(expected_entity_id) assert state is not None assert state.state == STATE_UNKNOWN @@ -62,8 +62,7 @@ async def test_linear_state_from_attribute(hass): config = { "compensation": { "test": { - "name": "compensation", - "entity_id": "sensor.uncompensated", + "source": "sensor.uncompensated", "attribute": "value", "data_points": [ [1.0, 2.0], @@ -73,6 +72,7 @@ async def test_linear_state_from_attribute(hass): } } } + 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) @@ -80,11 +80,11 @@ async def test_linear_state_from_attribute(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - entity_id = config[DOMAIN]["test"]["entity_id"] + entity_id = config[DOMAIN]["test"]["source"] hass.states.async_set(entity_id, 3, {"value": 4}) await hass.async_block_till_done() - state = hass.states.get("sensor.compensation") + state = hass.states.get(expected_entity_id) assert state is not None assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 5.0 @@ -95,7 +95,7 @@ async def test_linear_state_from_attribute(hass): hass.states.async_set(entity_id, 3, {"value": "bar"}) await hass.async_block_till_done() - state = hass.states.get("sensor.compensation") + state = hass.states.get(expected_entity_id) assert state is not None assert state.state == STATE_UNKNOWN @@ -106,8 +106,7 @@ async def test_quadratic_state(hass): config = { "compensation": { "test": { - "name": "compensation", - "entity_id": "sensor.temperature", + "source": "sensor.temperature", "data_points": [ [50, 3.3], [50, 2.8], @@ -131,16 +130,15 @@ async def test_quadratic_state(hass): } } 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_start() await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - - entity_id = config[DOMAIN]["test"]["entity_id"] + entity_id = config[DOMAIN]["test"]["source"] hass.states.async_set(entity_id, 43.2, {}) await hass.async_block_till_done() - state = hass.states.get("sensor.compensation") + state = hass.states.get("sensor.compensation_sensor_temperature") assert state is not None @@ -152,16 +150,14 @@ async def test_numpy_errors(hass, caplog): config = { "compensation": { "test": { - "name": "compensation", - "entity_id": "sensor.uncompensated", + "source": "sensor.uncompensated", "data_points": [ [1.0, 1.0], [1.0, 1.0], ], }, "test2": { - "name": "compensation2", - "entity_id": "sensor.uncompensated", + "source": "sensor.uncompensated2", "data_points": [ [0.0, 1.0], [0.0, 1.0], @@ -170,9 +166,9 @@ async def test_numpy_errors(hass, caplog): } } await async_setup_component(hass, DOMAIN, config) - await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_start() + await hass.async_block_till_done() assert "polyfit may be poorly conditioned" in caplog.text @@ -184,8 +180,7 @@ async def test_datapoints_greater_than_degree(hass, caplog): config = { "compensation": { "test": { - "name": "compensation", - "entity_id": "sensor.uncompensated", + "source": "sensor.uncompensated", "data_points": [ [1.0, 2.0], [2.0, 3.0], @@ -195,9 +190,9 @@ async def test_datapoints_greater_than_degree(hass, caplog): } } await async_setup_component(hass, DOMAIN, config) - await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_start() + await hass.async_block_till_done() assert "data_points must have at least 3 data_points" in caplog.text @@ -207,8 +202,7 @@ async def test_new_state_is_none(hass): config = { "compensation": { "test": { - "name": "compensation", - "entity_id": "sensor.uncompensated", + "source": "sensor.uncompensated", "data_points": [ [1.0, 2.0], [2.0, 3.0], @@ -218,14 +212,17 @@ async def test_new_state_is_none(hass): } } } + expected_entity_id = "sensor.compensation_sensor_uncompensated" - assert await async_setup_component(hass, DOMAIN, config) + 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("sensor.compensation").last_changed + 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("sensor.compensation").last_changed + assert last_changed == hass.states.get(expected_entity_id).last_changed From 8ba6559924f789ea46c8eb8ba359f6653eae13af Mon Sep 17 00:00:00 2001 From: Petro Date: Fri, 2 Apr 2021 21:15:53 +0000 Subject: [PATCH 20/21] Add name and fix unique_id --- homeassistant/components/compensation/__init__.py | 11 +++-------- homeassistant/components/compensation/const.py | 1 + homeassistant/components/compensation/sensor.py | 13 +++++++++++-- tests/components/compensation/test_sensor.py | 8 ++++---- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index 31bcf30760c1fe..5c270e681d4800 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( CONF_ATTRIBUTE, + CONF_NAME, CONF_SOURCE, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, @@ -23,6 +24,7 @@ CONF_PRECISION, DATA_COMPENSATION, DEFAULT_DEGREE, + DEFAULT_NAME, DEFAULT_PRECISION, DOMAIN, ) @@ -46,6 +48,7 @@ def datapoints_greater_than_degree(value: dict) -> dict: vol.Required(CONF_DATAPOINTS): [ vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)]) ], + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_ATTRIBUTE): cv.string, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int, @@ -105,14 +108,6 @@ async def async_setup(hass, config): } data[CONF_POLYNOMIAL] = np.poly1d(coefficients) - unique_id = data.get(CONF_UNIQUE_ID) - if unique_id is None: - identifier = data[CONF_SOURCE] - attribute = data.get(CONF_ATTRIBUTE) - data[CONF_UNIQUE_ID] = ( - identifier if attribute is None else f"{identifier}.{attribute}" - ) - hass.data[DATA_COMPENSATION][compensation] = data hass.async_create_task( diff --git a/homeassistant/components/compensation/const.py b/homeassistant/components/compensation/const.py index 79133dd36741ae..f116725883ecdb 100644 --- a/homeassistant/components/compensation/const.py +++ b/homeassistant/components/compensation/const.py @@ -12,4 +12,5 @@ DATA_COMPENSATION = "compensation_data" DEFAULT_DEGREE = 1 +DEFAULT_NAME = "Compensation" DEFAULT_PRECISION = 2 diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 3163ec3d70c78c..a2875d550447b1 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -5,6 +5,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_ATTRIBUTE, + CONF_NAME, CONF_SOURCE, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, @@ -33,8 +34,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities( [ CompensationSensor( - conf[CONF_SOURCE], conf.get(CONF_UNIQUE_ID), + conf[CONF_NAME], + conf[CONF_SOURCE], conf.get(CONF_ATTRIBUTE), conf[CONF_PRECISION], conf[CONF_POLYNOMIAL], @@ -49,8 +51,9 @@ class CompensationSensor(SensorEntity): def __init__( self, - source, unique_id, + name, + source, attribute, precision, polynomial, @@ -65,6 +68,7 @@ def __init__( self._coefficients = polynomial.coefficients.tolist() self._state = None self._unique_id = unique_id + self._name = name async def async_added_to_hass(self): """Handle added to Hass.""" @@ -81,6 +85,11 @@ def unique_id(self): """Return the unique id of this sensor.""" return self._unique_id + @property + def name(self): + """Return the name of the sensor.""" + return self._name + @property def should_poll(self): """No polling needed.""" diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index 3bd86280750ff7..84ca3dbb825db6 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -27,7 +27,7 @@ async def test_linear_state(hass): } } } - expected_entity_id = "sensor.compensation_sensor_uncompensated" + expected_entity_id = "sensor.compensation" assert await async_setup_component(hass, DOMAIN, config) assert await async_setup_component(hass, SENSOR_DOMAIN, config) @@ -72,7 +72,7 @@ async def test_linear_state_from_attribute(hass): } } } - expected_entity_id = "sensor.compensation_sensor_uncompensated_value" + expected_entity_id = "sensor.compensation" assert await async_setup_component(hass, DOMAIN, config) assert await async_setup_component(hass, SENSOR_DOMAIN, config) @@ -138,7 +138,7 @@ async def test_quadratic_state(hass): hass.states.async_set(entity_id, 43.2, {}) await hass.async_block_till_done() - state = hass.states.get("sensor.compensation_sensor_temperature") + state = hass.states.get("sensor.compensation") assert state is not None @@ -212,7 +212,7 @@ async def test_new_state_is_none(hass): } } } - expected_entity_id = "sensor.compensation_sensor_uncompensated" + expected_entity_id = "sensor.compensation" await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() From a83dd13a5ba22ad10f7a16eec49eb03bf708d3f9 Mon Sep 17 00:00:00 2001 From: Petro Date: Sat, 3 Apr 2021 12:24:17 +0000 Subject: [PATCH 21/21] removed conf name and auto construct it --- .../components/compensation/__init__.py | 3 --- .../components/compensation/sensor.py | 21 ++++++++++++++----- tests/components/compensation/test_sensor.py | 8 +++---- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index 5c270e681d4800..7d96905efa0aef 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -8,7 +8,6 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( CONF_ATTRIBUTE, - CONF_NAME, CONF_SOURCE, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, @@ -24,7 +23,6 @@ CONF_PRECISION, DATA_COMPENSATION, DEFAULT_DEGREE, - DEFAULT_NAME, DEFAULT_PRECISION, DOMAIN, ) @@ -48,7 +46,6 @@ def datapoints_greater_than_degree(value: dict) -> dict: vol.Required(CONF_DATAPOINTS): [ vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)]) ], - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_ATTRIBUTE): cv.string, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int, diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index a2875d550447b1..35ca07ce52215b 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -5,7 +5,6 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_ATTRIBUTE, - CONF_NAME, CONF_SOURCE, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, @@ -14,7 +13,13 @@ from homeassistant.core import callback from homeassistant.helpers.event import async_track_state_change_event -from .const import CONF_COMPENSATION, CONF_POLYNOMIAL, CONF_PRECISION, DATA_COMPENSATION +from .const import ( + CONF_COMPENSATION, + CONF_POLYNOMIAL, + CONF_PRECISION, + DATA_COMPENSATION, + DEFAULT_NAME, +) _LOGGER = logging.getLogger(__name__) @@ -31,13 +36,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= compensation = discovery_info[CONF_COMPENSATION] conf = hass.data[DATA_COMPENSATION][compensation] + source = conf[CONF_SOURCE] + attribute = conf.get(CONF_ATTRIBUTE) + name = f"{DEFAULT_NAME} {source}" + if attribute is not None: + name = f"{name} {attribute}" + async_add_entities( [ CompensationSensor( conf.get(CONF_UNIQUE_ID), - conf[CONF_NAME], - conf[CONF_SOURCE], - conf.get(CONF_ATTRIBUTE), + name, + source, + attribute, conf[CONF_PRECISION], conf[CONF_POLYNOMIAL], conf.get(CONF_UNIT_OF_MEASUREMENT), diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index 84ca3dbb825db6..3bd86280750ff7 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -27,7 +27,7 @@ async def test_linear_state(hass): } } } - expected_entity_id = "sensor.compensation" + expected_entity_id = "sensor.compensation_sensor_uncompensated" assert await async_setup_component(hass, DOMAIN, config) assert await async_setup_component(hass, SENSOR_DOMAIN, config) @@ -72,7 +72,7 @@ async def test_linear_state_from_attribute(hass): } } } - expected_entity_id = "sensor.compensation" + 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) @@ -138,7 +138,7 @@ async def test_quadratic_state(hass): hass.states.async_set(entity_id, 43.2, {}) await hass.async_block_till_done() - state = hass.states.get("sensor.compensation") + state = hass.states.get("sensor.compensation_sensor_temperature") assert state is not None @@ -212,7 +212,7 @@ async def test_new_state_is_none(hass): } } } - expected_entity_id = "sensor.compensation" + expected_entity_id = "sensor.compensation_sensor_uncompensated" await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done()