diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index 8d27f7188d2..0c42033006c 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -26,15 +26,15 @@ stores/caches the latest telegram and notifies the Entities that the telegram has been updated. """ import asyncio -import logging from datetime import timedelta +import logging -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import voluptuous as vol _LOGGER = logging.getLogger(__name__) @@ -65,30 +65,37 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # Suppress logging logging.getLogger('dsmr_parser').setLevel(logging.ERROR) - from dsmr_parser import obis_references as obis + from dsmr_parser import obis_references as obis_ref from dsmr_parser.protocol import create_dsmr_reader dsmr_version = config[CONF_DSMR_VERSION] # Define list of name,obis mappings to generate entities obis_mapping = [ - ['Power Consumption', obis.CURRENT_ELECTRICITY_USAGE], - ['Power Production', obis.CURRENT_ELECTRICITY_DELIVERY], - ['Power Tariff', obis.ELECTRICITY_ACTIVE_TARIFF], - ['Power Consumption (low)', obis.ELECTRICITY_USED_TARIFF_1], - ['Power Consumption (normal)', obis.ELECTRICITY_USED_TARIFF_2], - ['Power Production (low)', obis.ELECTRICITY_DELIVERED_TARIFF_1], - ['Power Production (normal)', obis.ELECTRICITY_DELIVERED_TARIFF_2], + ['Power Consumption', obis_ref.CURRENT_ELECTRICITY_USAGE], + ['Power Production', obis_ref.CURRENT_ELECTRICITY_DELIVERY], + ['Power Tariff', obis_ref.ELECTRICITY_ACTIVE_TARIFF], + ['Power Consumption (low)', obis_ref.ELECTRICITY_USED_TARIFF_1], + ['Power Consumption (normal)', obis_ref.ELECTRICITY_USED_TARIFF_2], + ['Power Production (low)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], + ['Power Production (normal)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], ] - # Protocol version specific obis - if dsmr_version == '4': - obis_mapping.append(['Gas Consumption', obis.HOURLY_GAS_METER_READING]) - else: - obis_mapping.append(['Gas Consumption', obis.GAS_METER_READING]) # Generate device entities devices = [DSMREntity(name, obis) for name, obis in obis_mapping] + # Protocol version specific obis + if dsmr_version == '4': + gas_obis = obis_ref.HOURLY_GAS_METER_READING + else: + gas_obis = obis_ref.GAS_METER_READING + + # add gas meter reading and derivative for usage + devices += [ + DSMREntity('Gas Consumption', gas_obis), + DerivativeDSMREntity('Hourly Gas Consumption', gas_obis), + ] + yield from async_add_devices(devices) def update_entities_telegram(telegram): @@ -151,7 +158,10 @@ class DSMREntity(Entity): if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF: return self.translate_tariff(value) else: - return value + if value: + return value + else: + return STATE_UNKNOWN @property def unit_of_measurement(self): @@ -168,4 +178,55 @@ class DSMREntity(Entity): elif value == '0001': return 'low' else: - return None + return STATE_UNKNOWN + + +class DerivativeDSMREntity(DSMREntity): + """Calculated derivative for values where the DSMR doesn't offer one. + + Gas readings are only reported per hour and don't offer a rate only + the current meter reading. This entity converts subsequents readings + into a hourly rate. + """ + + _previous_reading = None + _previous_timestamp = None + _state = STATE_UNKNOWN + + @property + def state(self): + """Return the calculated current hourly rate.""" + return self._state + + @asyncio.coroutine + def async_update(self): + """Recalculate hourly rate if timestamp has changed. + + DSMR updates gas meter reading every hour. Along with the + new value a timestamp is provided for the reading. Test + if the last known timestamp differs from the current one + then calculate a new rate for the previous hour. + """ + # check if the timestamp for the object differs from the previous one + timestamp = self.get_dsmr_object_attr('datetime') + if timestamp and timestamp != self._previous_timestamp: + current_reading = self.get_dsmr_object_attr('value') + + if self._previous_reading is None: + # can't calculate rate without previous datapoint + # just store current point + pass + else: + # recalculate the rate + diff = current_reading - self._previous_reading + self._state = diff + + self._previous_reading = current_reading + self._previous_timestamp = timestamp + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, per hour, if any.""" + unit = self.get_dsmr_object_attr('unit') + if unit: + return unit + '/h' diff --git a/tests/components/sensor/test_dsmr.py b/tests/components/sensor/test_dsmr.py index 166a4af9657..e76b26a811b 100644 --- a/tests/components/sensor/test_dsmr.py +++ b/tests/components/sensor/test_dsmr.py @@ -9,6 +9,8 @@ from decimal import Decimal from unittest.mock import Mock from homeassistant.bootstrap import async_setup_component +from homeassistant.components.sensor.dsmr import DerivativeDSMREntity +from homeassistant.const import STATE_UNKNOWN from tests.common import assert_setup_component @@ -62,3 +64,37 @@ def test_default_setup(hass, monkeypatch): power_tariff = hass.states.get('sensor.power_tariff') assert power_tariff.state == 'low' assert power_tariff.attributes.get('unit_of_measurement') is None + + +def test_derivative(): + """Test calculation of derivative value.""" + from dsmr_parser.objects import MBusObject + + entity = DerivativeDSMREntity('test', '1.0.0') + yield from entity.async_update() + + assert entity.state == STATE_UNKNOWN, 'initial state not unknown' + + entity.telegram = { + '1.0.0': MBusObject([ + {'value': 1}, + {'value': 1, 'unit': 'm3'}, + ]) + } + yield from entity.async_update() + + assert entity.state == STATE_UNKNOWN, \ + 'state after first update shoudl still be unknown' + + entity.telegram = { + '1.0.0': MBusObject([ + {'value': 2}, + {'value': 2, 'unit': 'm3'}, + ]) + } + yield from entity.async_update() + + assert entity.state == 1, \ + 'state should be difference between first and second update' + + assert entity.unit_of_measurement == 'm3/h'