Dsmr hourly gas usage. (#4609)
* Hourly rate of Gas consumption. Use proper unknown state. * Import unknown state constant. * doh * Cleanup device add. * Fix lint. * Add test for derivative calculation. * Remove conflict. * Document and move calculation into update call.pull/4708/head
parent
a099430834
commit
776e53a7f0
|
@ -26,15 +26,15 @@ stores/caches the latest telegram and notifies the Entities that the telegram
|
||||||
has been updated.
|
has been updated.
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
|
||||||
from datetime import timedelta
|
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.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
|
from homeassistant.helpers.entity import Entity
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -65,30 +65,37 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
# Suppress logging
|
# Suppress logging
|
||||||
logging.getLogger('dsmr_parser').setLevel(logging.ERROR)
|
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
|
from dsmr_parser.protocol import create_dsmr_reader
|
||||||
|
|
||||||
dsmr_version = config[CONF_DSMR_VERSION]
|
dsmr_version = config[CONF_DSMR_VERSION]
|
||||||
|
|
||||||
# Define list of name,obis mappings to generate entities
|
# Define list of name,obis mappings to generate entities
|
||||||
obis_mapping = [
|
obis_mapping = [
|
||||||
['Power Consumption', obis.CURRENT_ELECTRICITY_USAGE],
|
['Power Consumption', obis_ref.CURRENT_ELECTRICITY_USAGE],
|
||||||
['Power Production', obis.CURRENT_ELECTRICITY_DELIVERY],
|
['Power Production', obis_ref.CURRENT_ELECTRICITY_DELIVERY],
|
||||||
['Power Tariff', obis.ELECTRICITY_ACTIVE_TARIFF],
|
['Power Tariff', obis_ref.ELECTRICITY_ACTIVE_TARIFF],
|
||||||
['Power Consumption (low)', obis.ELECTRICITY_USED_TARIFF_1],
|
['Power Consumption (low)', obis_ref.ELECTRICITY_USED_TARIFF_1],
|
||||||
['Power Consumption (normal)', obis.ELECTRICITY_USED_TARIFF_2],
|
['Power Consumption (normal)', obis_ref.ELECTRICITY_USED_TARIFF_2],
|
||||||
['Power Production (low)', obis.ELECTRICITY_DELIVERED_TARIFF_1],
|
['Power Production (low)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_1],
|
||||||
['Power Production (normal)', obis.ELECTRICITY_DELIVERED_TARIFF_2],
|
['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
|
# Generate device entities
|
||||||
devices = [DSMREntity(name, obis) for name, obis in obis_mapping]
|
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)
|
yield from async_add_devices(devices)
|
||||||
|
|
||||||
def update_entities_telegram(telegram):
|
def update_entities_telegram(telegram):
|
||||||
|
@ -151,7 +158,10 @@ class DSMREntity(Entity):
|
||||||
if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF:
|
if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF:
|
||||||
return self.translate_tariff(value)
|
return self.translate_tariff(value)
|
||||||
else:
|
else:
|
||||||
return value
|
if value:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
|
@ -168,4 +178,55 @@ class DSMREntity(Entity):
|
||||||
elif value == '0001':
|
elif value == '0001':
|
||||||
return 'low'
|
return 'low'
|
||||||
else:
|
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'
|
||||||
|
|
|
@ -9,6 +9,8 @@ from decimal import Decimal
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from homeassistant.bootstrap import async_setup_component
|
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
|
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')
|
power_tariff = hass.states.get('sensor.power_tariff')
|
||||||
assert power_tariff.state == 'low'
|
assert power_tariff.state == 'low'
|
||||||
assert power_tariff.attributes.get('unit_of_measurement') is None
|
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'
|
||||||
|
|
Loading…
Reference in New Issue