From 982807fbc15703f0ccf68305581f2931625abe3b Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 13 Feb 2019 01:13:00 +0000 Subject: [PATCH 1/3] add integration method and respective new methods --- .../components/sensor/integration.py | 29 ++++- tests/components/sensor/test_integration.py | 104 ++++++++++++++++++ 2 files changed, 127 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/integration.py b/homeassistant/components/sensor/integration.py index 9426730be359bf..32ba0565603648 100644 --- a/homeassistant/components/sensor/integration.py +++ b/homeassistant/components/sensor/integration.py @@ -26,6 +26,12 @@ CONF_UNIT_PREFIX = 'unit_prefix' CONF_UNIT_TIME = 'unit_time' CONF_UNIT_OF_MEASUREMENT = 'unit' +CONF_INTEGRATION_METHOD = 'method' + +TRAPEZOIDAL_METHOD = 'trapezoidal' +LEFT_METHOD = 'left' +RIGHT_METHOD = 'right' +INTEGRATION_METHOD = [TRAPEZOIDAL_METHOD, LEFT_METHOD, RIGHT_METHOD] # SI Metric prefixes UNIT_PREFIXES = {None: 1, @@ -49,7 +55,9 @@ vol.Optional(CONF_ROUND_DIGITS, default=DEFAULT_ROUND): vol.Coerce(int), vol.Optional(CONF_UNIT_PREFIX, default=None): vol.In(UNIT_PREFIXES), vol.Optional(CONF_UNIT_TIME, default='h'): vol.In(UNIT_TIME), - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_INTEGRATION_METHOD, default=TRAPEZOIDAL_METHOD): + vol.In(INTEGRATION_METHOD) }) @@ -61,7 +69,8 @@ async def async_setup_platform(hass, config, async_add_entities, config[CONF_ROUND_DIGITS], config[CONF_UNIT_PREFIX], config[CONF_UNIT_TIME], - config.get(CONF_UNIT_OF_MEASUREMENT)) + config.get(CONF_UNIT_OF_MEASUREMENT), + config[CONF_INTEGRATION_METHOD]) async_add_entities([integral]) @@ -70,11 +79,12 @@ class IntegrationSensor(RestoreEntity): """Representation of an integration sensor.""" def __init__(self, source_entity, name, round_digits, unit_prefix, - unit_time, unit_of_measurement): + unit_time, unit_of_measurement, integration_method): """Initialize the integration sensor.""" self._sensor_source_id = source_entity self._round_digits = round_digits self._state = 0 + self._method = integration_method self._name = name if name is not None\ else '{} integral'.format(source_entity) @@ -117,12 +127,19 @@ def calc_integration(entity, old_state, new_state): try: # integration as the Riemann integral of previous measures. + area = 0 elapsed_time = (new_state.last_updated - old_state.last_updated).total_seconds() - area = (Decimal(new_state.state) - + Decimal(old_state.state))*Decimal(elapsed_time)/2 - integral = area / (self._unit_prefix * self._unit_time) + if self._method == TRAPEZOIDAL_METHOD: + area = (Decimal(new_state.state) + + Decimal(old_state.state))*Decimal(elapsed_time)/2 + elif self._method == LEFT_METHOD: + area = Decimal(old_state.state)*Decimal(elapsed_time) + elif self._method == RIGHT_METHOD: + area = Decimal(new_state.state)*Decimal(elapsed_time) + + integral = area / (self._unit_prefix * self._unit_time) assert isinstance(integral, Decimal) except ValueError as err: _LOGGER.warning("While calculating integration: %s", err) diff --git a/tests/components/sensor/test_integration.py b/tests/components/sensor/test_integration.py index bb4a02c042bf50..7f02d59f5913db 100644 --- a/tests/components/sensor/test_integration.py +++ b/tests/components/sensor/test_integration.py @@ -39,6 +39,110 @@ async def test_state(hass): assert state.attributes.get('unit_of_measurement') == 'kWh' +async def test_trapezoidal(hass): + """Test integration sensor state.""" + config = { + 'sensor': { + 'platform': 'integration', + 'name': 'integration', + 'source': 'sensor.power', + 'unit': 'kWh', + 'round': 2, + } + } + + assert await async_setup_component(hass, 'sensor', config) + + entity_id = config['sensor']['source'] + hass.states.async_set(entity_id, 0, {}) + await hass.async_block_till_done() + + # Testing a power sensor with non-monotonic intervals and values + for time, value in [(20, 10), (30, 30), (40, 5), (50, 0)]: + now = dt_util.utcnow() + timedelta(minutes=time) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, value, {}, force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.integration') + assert state is not None + + assert round(float(state.state), config['sensor']['round']) == 8.33 + + assert state.attributes.get('unit_of_measurement') == 'kWh' + + +async def test_left(hass): + """Test integration sensor state with left reimann method.""" + config = { + 'sensor': { + 'platform': 'integration', + 'name': 'integration', + 'method': 'left', + 'source': 'sensor.power', + 'unit': 'kWh', + 'round': 2, + } + } + + assert await async_setup_component(hass, 'sensor', config) + + entity_id = config['sensor']['source'] + hass.states.async_set(entity_id, 0, {}) + await hass.async_block_till_done() + + # Testing a power sensor with non-monotonic intervals and values + for time, value in [(20, 10), (30, 30), (40, 5), (50, 0)]: + now = dt_util.utcnow() + timedelta(minutes=time) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, value, {}, force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.integration') + assert state is not None + + assert round(float(state.state), config['sensor']['round']) == 7.5 + + assert state.attributes.get('unit_of_measurement') == 'kWh' + + +async def test_right(hass): + """Test integration sensor state with left reimann method.""" + config = { + 'sensor': { + 'platform': 'integration', + 'name': 'integration', + 'method': 'right', + 'source': 'sensor.power', + 'unit': 'kWh', + 'round': 2, + } + } + + assert await async_setup_component(hass, 'sensor', config) + + entity_id = config['sensor']['source'] + hass.states.async_set(entity_id, 0, {}) + await hass.async_block_till_done() + + # Testing a power sensor with non-monotonic intervals and values + for time, value in [(20, 10), (30, 30), (40, 5), (50, 0)]: + now = dt_util.utcnow() + timedelta(minutes=time) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, value, {}, force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.integration') + assert state is not None + + assert round(float(state.state), config['sensor']['round']) == 9.17 + + assert state.attributes.get('unit_of_measurement') == 'kWh' + + async def test_prefix(hass): """Test integration sensor state using a power source.""" config = { From 24800170ade5239e9d11d6ffc7bae62c92e3607e Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 13 Feb 2019 21:06:04 +0000 Subject: [PATCH 2/3] ack @ottowinter tip --- homeassistant/components/sensor/integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/integration.py b/homeassistant/components/sensor/integration.py index 32ba0565603648..ceccc9bf83b700 100644 --- a/homeassistant/components/sensor/integration.py +++ b/homeassistant/components/sensor/integration.py @@ -57,7 +57,7 @@ vol.Optional(CONF_UNIT_TIME, default='h'): vol.In(UNIT_TIME), vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_INTEGRATION_METHOD, default=TRAPEZOIDAL_METHOD): - vol.In(INTEGRATION_METHOD) + vol.In(INTEGRATION_METHOD), }) From fc74431f33bed5bef4be4d3d2d5c2e1b76322ca8 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 13 Feb 2019 21:07:24 +0000 Subject: [PATCH 3/3] align const name with value --- homeassistant/components/sensor/integration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/integration.py b/homeassistant/components/sensor/integration.py index ceccc9bf83b700..9250c8dde055ee 100644 --- a/homeassistant/components/sensor/integration.py +++ b/homeassistant/components/sensor/integration.py @@ -26,7 +26,7 @@ CONF_UNIT_PREFIX = 'unit_prefix' CONF_UNIT_TIME = 'unit_time' CONF_UNIT_OF_MEASUREMENT = 'unit' -CONF_INTEGRATION_METHOD = 'method' +CONF_METHOD = 'method' TRAPEZOIDAL_METHOD = 'trapezoidal' LEFT_METHOD = 'left' @@ -56,7 +56,7 @@ vol.Optional(CONF_UNIT_PREFIX, default=None): vol.In(UNIT_PREFIXES), vol.Optional(CONF_UNIT_TIME, default='h'): vol.In(UNIT_TIME), vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_INTEGRATION_METHOD, default=TRAPEZOIDAL_METHOD): + vol.Optional(CONF_METHOD, default=TRAPEZOIDAL_METHOD): vol.In(INTEGRATION_METHOD), }) @@ -70,7 +70,7 @@ async def async_setup_platform(hass, config, async_add_entities, config[CONF_UNIT_PREFIX], config[CONF_UNIT_TIME], config.get(CONF_UNIT_OF_MEASUREMENT), - config[CONF_INTEGRATION_METHOD]) + config[CONF_METHOD]) async_add_entities([integral])