-
-
Notifications
You must be signed in to change notification settings - Fork 29.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DSMR sensor #4309
Merged
Merged
DSMR sensor #4309
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
3c1fd15
Initial implemenation of DSMR component.
5b3f0ba
Fix linting
aed7627
Remove protocol V2.2 support until merged upstream.
4544685
Generate requirements using script.
ce70497
Use updated dsmr-parser with protocol 2.2 support.
fc6e732
Add tests.
9c86d0a
Isort and input validation.
e4594f1
Add entities for gas and actual meter reading. Error handling. Use Th…
27a4e01
Implement non-blocking serial reader.
1cf1341
Improve logging.
aabbe6e
Merge entities into one, add icons, fix tests for asyncio.
c5de690
Add error logging for serial reader.
8f30b51
Refactoring and documentation.
42d7e23
Use `port` configuration key.
5367334
DSMR V2.2 seems to conflict in explaining which tariff is high and low.
873cbd4
Refactor to use asyncio.Protocol instead of loop+queue.
749de01
Fix requirements
30137d4
Close transport when HA stops.
95e5a83
Cleanup.
eb6c853
Include as dependency for testing (until merged upstream.)
f5e9ccc
Fix style.
0762fca
Update setup.cfg
balloob File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
""" | ||
Support for Dutch Smart Meter Requirements. | ||
|
||
Also known as: Smartmeter or P1 port. | ||
|
||
For more details about this platform, please refer to the documentation at | ||
https://home-assistant.io/components/sensor.dsmr/ | ||
|
||
Technical overview: | ||
|
||
DSMR is a standard to which Dutch smartmeters must comply. It specifies that | ||
the smartmeter must send out a 'telegram' every 10 seconds over a serial port. | ||
|
||
The contents of this telegram differ between version but they generally consist | ||
of lines with 'obis' (Object Identification System, a numerical ID for a value) | ||
followed with the value and unit. | ||
|
||
This module sets up a asynchronous reading loop using the `dsmr_parser` module | ||
which waits for a complete telegram, parser it and puts it on an async queue as | ||
a dictionary of `obis`/object mapping. The numeric value and unit of each value | ||
can be read from the objects attributes. Because the `obis` are know for each | ||
DSMR version the Entities for this component are create during bootstrap. | ||
|
||
Another loop (DSMR class) is setup which reads the telegram queue, | ||
stores/caches the latest telegram and notifies the Entities that the telegram | ||
has been updated. | ||
""" | ||
|
||
import asyncio | ||
import logging | ||
from datetime import timedelta | ||
|
||
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.helpers.entity import Entity | ||
|
||
DOMAIN = 'dsmr' | ||
|
||
REQUIREMENTS = [ | ||
'https://github.com/aequitas/dsmr_parser/archive/async_protocol.zip' | ||
'#dsmr_parser==0.4' | ||
] | ||
|
||
# Smart meter sends telegram every 10 seconds | ||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) | ||
|
||
CONF_DSMR_VERSION = 'dsmr_version' | ||
DEFAULT_PORT = '/dev/ttyUSB0' | ||
DEFAULT_DSMR_VERSION = '2.2' | ||
|
||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ | ||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, | ||
vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( | ||
cv.string, vol.In(['4', '2.2'])), | ||
}) | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
ICON_POWER = 'mdi:flash' | ||
ICON_GAS = 'mdi:fire' | ||
|
||
|
||
@asyncio.coroutine | ||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): | ||
"""Setup DSMR sensors.""" | ||
# suppres logging | ||
logging.getLogger('dsmr_parser').setLevel(logging.ERROR) | ||
|
||
from dsmr_parser import obis_references as obis | ||
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], | ||
] | ||
# 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] | ||
|
||
# setup devices | ||
yield from async_add_devices(devices) | ||
|
||
def update_entities_telegram(telegram): | ||
"""Update entities with latests telegram & trigger state update.""" | ||
# make all device entities aware of new telegram | ||
for device in devices: | ||
device.telegram = telegram | ||
hass.async_add_job(device.async_update_ha_state) | ||
|
||
# creates a asyncio.Protocol for reading DSMR telegrams from serial | ||
# and calls update_entities_telegram to update entities on arrival | ||
dsmr = create_dsmr_reader(config[CONF_PORT], config[CONF_DSMR_VERSION], | ||
update_entities_telegram, loop=hass.loop) | ||
|
||
# start DSMR asycnio.Protocol reader | ||
transport, _ = yield from hass.loop.create_task(dsmr) | ||
|
||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, transport.close) | ||
|
||
|
||
class DSMREntity(Entity): | ||
"""Entity reading values from DSMR telegram.""" | ||
|
||
def __init__(self, name, obis): | ||
""""Initialize entity.""" | ||
# human readable name | ||
self._name = name | ||
# DSMR spec. value identifier | ||
self._obis = obis | ||
self.telegram = {} | ||
|
||
def get_dsmr_object_attr(self, attribute): | ||
"""Read attribute from last received telegram for this DSMR object.""" | ||
# make sure telegram contains an object for this entities obis | ||
if self._obis not in self.telegram: | ||
return None | ||
|
||
# get the attibute value if the object has it | ||
dsmr_object = self.telegram[self._obis] | ||
return getattr(dsmr_object, attribute, None) | ||
|
||
@property | ||
def name(self): | ||
"""Return the name of the sensor.""" | ||
return self._name | ||
|
||
@property | ||
def icon(self): | ||
"""Icon to use in the frontend, if any.""" | ||
if 'Power' in self._name: | ||
return ICON_POWER | ||
elif 'Gas' in self._name: | ||
return ICON_GAS | ||
|
||
@property | ||
def state(self): | ||
"""Return the state of sensor, if available, translate if needed.""" | ||
from dsmr_parser import obis_references as obis | ||
|
||
value = self.get_dsmr_object_attr('value') | ||
|
||
if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF: | ||
return self.translate_tariff(value) | ||
else: | ||
return value | ||
|
||
@property | ||
def unit_of_measurement(self): | ||
"""Return the unit of measurement of this entity, if any.""" | ||
return self.get_dsmr_object_attr('unit') | ||
|
||
@staticmethod | ||
def translate_tariff(value): | ||
"""Convert 2/1 to normal/low.""" | ||
# DSMR V2.2: Note: Tariff code 1 is used for low tariff | ||
# and tariff code 2 is used for normal tariff. | ||
|
||
if value == '0002': | ||
return 'normal' | ||
elif value == '0001': | ||
return 'low' | ||
else: | ||
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
"""Test for DSMR components. | ||
|
||
Tests setup of the DSMR component and ensure incoming telegrams cause Entity | ||
to be updated with new values. | ||
""" | ||
|
||
import asyncio | ||
from decimal import Decimal | ||
from unittest.mock import Mock | ||
|
||
from homeassistant.bootstrap import async_setup_component | ||
from tests.common import assert_setup_component | ||
|
||
|
||
@asyncio.coroutine | ||
def test_default_setup(hass, monkeypatch): | ||
"""Test the default setup.""" | ||
from dsmr_parser.obis_references import ( | ||
CURRENT_ELECTRICITY_USAGE, | ||
ELECTRICITY_ACTIVE_TARIFF, | ||
) | ||
from dsmr_parser.objects import CosemObject | ||
|
||
config = {'platform': 'dsmr'} | ||
|
||
telegram = { | ||
CURRENT_ELECTRICITY_USAGE: CosemObject([ | ||
{'value': Decimal('0.1'), 'unit': 'kWh'} | ||
]), | ||
ELECTRICITY_ACTIVE_TARIFF: CosemObject([ | ||
{'value': '0001', 'unit': ''} | ||
]), | ||
} | ||
|
||
# mock for injecting DSMR telegram | ||
dsmr = Mock(return_value=Mock()) | ||
monkeypatch.setattr('dsmr_parser.protocol.create_dsmr_reader', dsmr) | ||
|
||
with assert_setup_component(1): | ||
yield from async_setup_component(hass, 'sensor', | ||
{'sensor': config}) | ||
|
||
telegram_callback = dsmr.call_args_list[0][0][2] | ||
|
||
# make sure entities have been created and return 'unknown' state | ||
power_consumption = hass.states.get('sensor.power_consumption') | ||
assert power_consumption.state == 'unknown' | ||
assert power_consumption.attributes.get('unit_of_measurement') is None | ||
|
||
# simulate a telegram pushed from the smartmeter and parsed by dsmr_parser | ||
telegram_callback(telegram) | ||
|
||
# after receiving telegram entities need to have the chance to update | ||
yield from asyncio.sleep(0, loop=hass.loop) | ||
|
||
# ensure entities have new state value after incoming telegram | ||
power_consumption = hass.states.get('sensor.power_consumption') | ||
assert power_consumption.state == '0.1' | ||
assert power_consumption.attributes.get('unit_of_measurement') is 'kWh' | ||
|
||
# tariff should be translated in human readable and have no unit | ||
power_tariff = hass.states.get('sensor.power_tariff') | ||
assert power_tariff.state == 'low' | ||
assert power_tariff.attributes.get('unit_of_measurement') is None |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know if this will be merged upstream? Or are you planning on maintaining an async fork?
This also needs a version at the end
...zip#dsmr-parser==0.4
for example.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't know for sure yet, but the maintainer has been very keen to merge my changes so far so I don't foresee issue there. If not I will create my fork. I added a todo to this PR to ensure this gets taken care of.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merged upstream, just waiting for new version to be deployed: ndokter/dsmr_parser#7