diff --git a/homeassistant/components/sensor/integration.py b/homeassistant/components/sensor/integration.py index 9426730be35..9250c8dde05 100644 --- a/homeassistant/components/sensor/integration.py +++ b/homeassistant/components/sensor/integration.py @@ -26,6 +26,12 @@ CONF_ROUND_DIGITS = 'round' CONF_UNIT_PREFIX = 'unit_prefix' CONF_UNIT_TIME = 'unit_time' CONF_UNIT_OF_MEASUREMENT = 'unit' +CONF_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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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_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_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 @@ class IntegrationSensor(RestoreEntity): 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 bb4a02c042b..7f02d59f591 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 = {