Skip to content
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 22 commits into from
Nov 23, 2016
Merged

DSMR sensor #4309

merged 22 commits into from
Nov 23, 2016

Conversation

aequitas
Copy link
Contributor

@aequitas aequitas commented Nov 8, 2016

This is my initial version of an sensor implementation for the DSMR (Dutch smart meter protocol). In its current state it is blocking IO which I intent to fix first and it is dependent on contributions to dsmr_parser which need to be released upstream.

I would like to share this as a WIP and get feedback about if I am on track as this is the first component implementation I've written.

Example entry for configuration.yaml (if applicable):

sensor:
  - name: dsmr
    platform: dsmr
    device: /dev/ttyUSB1

Checklist:

Todos:

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • Local tests with tox run successfully.
  • New dependencies have been added to the REQUIREMENTS variable.
  • New dependencies are only imported inside functions that use them.
  • New dependencies have been added to requirements_all.txt by running script/gen_requirements_all.py.
  • New files were added to .coveragerc.
  • Tests have been added to verify that the new code works.
  • Entity icons
  • Error handling and recovery
  • Wait for asyncio protocol to be merged upstream in dsmr_parser, update REQUIREMENTS


_LOGGER.info('got new telegram')

yield from asyncio.sleep(10, loop=self.hass.loop)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this sleep for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should have documented this, but this is a temporary poor-mans semi-non-blocking implementation: the smartmeter sends 'telegrams' every 10 seconds. So the update only has to check every 10 seconds after the first telegram is received. This will be removed if non-blocking is implemented.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aequitas you'll be better off using @Throttle in that case 👍

self._telegram = {}

@asyncio.coroutine
def async_update(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, this isn't making use of any asyncio functionality - this probably shouldn't be async as the update func should be run in an executor by the core.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm quiet new to the whole asyncio stuff in python3 but I thought this was the new way to go with components according to (https://home-assistant.io/developers/asyncio/) and since the current library (dsmr-parser) is implemented blocking I figured I might implement it non-blocking using asyncio (see todo).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aequitas if you're planning on making dsmr-parser then your current implementation will be appropriate.

@fabaff fabaff changed the title Dsmr DSMR sensor Nov 10, 2016
REQUIREMENTS = ['dsmr-parser==0.2']

CONF_DSMR_VERSION = 'dsmr_version'
DEFAULT_DEVICE = '/dev/ttyUSB0'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't recommend to do that because this will most likely not work on macOS and Windows.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Mac it uses a tty device name with a random number I believe, will verify that, for windows it would default to COM0 I guess? So for Mac/Windows you would have to set the device but for Linux it would work without config if its the only device. There should be no harm in keeping a default in that case I think.

On this topic, I have been pondering about a auto discovery implementation for serial devices. It is out of scope for this PR but I think it could be doable. I want to implement it for my own setup as I have some other devices that get assigned a device name in random order at boot.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with that default is that it would be hard to debug for a no-Linux user.

DEFAULT_DSMR_VERSION = '4'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For serial ports we use port: for the configuration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this kind of stuff documented somewhere? Because I took the zigbee module as an example which uses 'device' so my guess was that was the convention https://github.com/home-assistant/home-assistant/blob/ece58ce78fe44b521122e3f41b79e670010bc905/homeassistant/components/zigbee.py#L30

and port seems to be generally used only in tcp/udp context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's not documented. I'm trying to establish a common base for the configuration and not use usb_path, filename, and others. It's a connection over a serial port. Even better than port would serial_port be, distinguish between the network port and the physical port.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason it is not outlined in a document yet?

In my opinion if you want to work toward a convention for (for example) naming you outline it first in a document, this should make it easy for (new) contributors to learn about what the conventions are. Now I had to learn this convention through a pullrequest review, as the only other information I could go on is other components.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rename the config to port, let me know if you want me to take initiative on documenting the conventions.

@aequitas
Copy link
Contributor Author

I added the implementation for a non-blocking reader, however it needs upstream contribution (ndokter/dsmr_parser#3) before it can pass the tests. Feel free to comment on the new code.

Tests still need to be refactored.

@@ -0,0 +1 @@
layout python3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep this? Add it to .gitignore if needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this got included by accident.

hass.loop.create_task(dsmr.read_telegrams(queue))


class DSMR:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be an interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the question a little further?

@property
def state(self):
"""Return the state of the sensor, if available."""
return getattr(self._interface.telegram.get(self._obis, {}),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be cached in an update method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The call itself only reads the value of telegram and does not perform any IO or update sequence. What would be the reason to cache the value in this case?

]),
}

# with patch('homeassistant.components.sensor.dsmr.DSMR.read_telegram',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this commented?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still need to refactor the test suite after de code has been refactored to non-blocking. This will be addresses (see todo).

@aequitas
Copy link
Contributor Author

Refactored the tests for asyncio and added error handling. Also simplified/refactored some other parts of the code. I have all todos I intended for this feature completed. Could you please review again and let me know what needs to be changed before it can be accepted.

devices += [DSMREntity(name, obis, dsmr) for name, obis in obis_mapping]

# setup devices
yield from hass.loop.create_task(async_add_devices(devices))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async_add_devices is a coroutine function, no need to wrap it in create_task

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ofc, I still need to get a little accustomed to the framework and asyncio.

reader.add_done_callback(handle_error)

# add task to receive telegrams and update entities
hass.loop.create_task(dsmr.read_telegrams(queue))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use hass.async_add_job so that we can keep track of it (and be able to use hass.block_till_done())

Also, please cancel this coroutine when HOMEASSISTANT_STOP event is fired.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try to make that work.

Could you tell me if I am using the right pattern here? Hardware wise the Smartmeter is connection via a usb serial device, and the smartmeter sends a update every 10 seconds. I wanted to get this to work with some kind of async generator which would yield on waiting for new serial input. However my short research into asyncio indicated async generators are not supported until python 3.6. I was unable to find a different way to create something like a async generator for python3.4. So I resorted to a task with a output queue. But maybe there is a better (simpler) way to implement this which does away with the queue and the need to listen for HOMEASSISTANT_STOP.

"""Return the state of sensor, if available, translate if needed."""
from dsmr_parser import obis_references as obis

value = getattr(self._interface.telegram.get(self._obis, {}),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear to me if these calls are doing I/O. They shouldn't (as we're inside async land)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Someone else also commented on this. I will refactor/document this piece a bit to make it more clear it is not doing I/O.


# mock queue for injecting DSMR telegram
queue = asyncio.Queue(loop=hass.loop)
monkeypatch.setattr('asyncio.Queue', lambda: queue)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. Didn't know about monkeypatch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of cool stuff in pytest. Thanks for going with that framework (and Tox). In my opinion it is the best one out there atm, but it doesn't force you into one style of writing tests as it also supports unittest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, have you considered using https://github.com/klen/pylama for the project? It combines a lot of the linting tools used in this project into a single consistent interface.

@balloob balloob self-assigned this Nov 13, 2016
- refactor asyncio reader task to make sure it stops with HA
- document general principle of this component
- refactor entity reading to be more clear
- remove cruft from split entity implementation
@aequitas
Copy link
Contributor Author

@balloob I refactored to address the issues you mentioned. Thanks for pointing out the required structures in the framework. I did not find good examples myself in the other components because there was just to much :). My assumption was hass/asyncio would take care of the stopping of the tasks automatically..

I added some additional overall documentation at the top of the module.

Let me know if there is anything else that needs to be done.

@aequitas
Copy link
Contributor Author

Strange, this build failed for py34 but another travis build on the same commit succeeded (https://travis-ci.org/aequitas/home-assistant/builds/175541781). This one might succeed if retried again. Error/failure is not on my testsuite btw.

Is it common for these kind of test instabilities to pop up in this framework? Or could it be a bug/race-condition introduced by my code?

http://www.netbeheernederland.nl/themas/hotspot/hotspot-documenten/?dossierid=11010056&title=Slimme%20meter&onderdeel=Documenten
> DSMR v2.2 Final P1
>> 6.1: table vs table note

    Meter Reading electricity delivered to client normal tariff) in 0,01 kWh - 1-0:1.8.1.255
    Meter Reading electricity delivered to client (low tariff) in 0,01 kWh - 1-0:1.8.2.255

    Note: Tariff code 1 is used for low tariff and tariff code 2 is used for normal tariff.
@aequitas
Copy link
Contributor Author

Currently this components is being tested by a few persons in this thread: https://community.home-assistant.io/t/support-for-reading-dutch-smart-meter-electricity-gas-p1-port/1676

Some people have issues regarding serial port settings which can differ depending on the meter type and usb hardware used. I am working on finding a solution for this.

@henkvdt
Copy link

henkvdt commented Nov 17, 2016

I might be wrong, but I think the kWh and kW units of measurement are switched: The current usage shows kW, but this should be kWh (usage per hour). The meter reading shows kWh, but this should be kW (absolute number)

@aequitas
Copy link
Contributor Author

@henkvdt the kW and kWh units are taken directly from the DSMR telegram. So the telegram would be wrong in that case.

The current usage shows how much power is being used at this moment in kilowats, the meter reading is how much energy has been consumed.
http://www.energylens.com/articles/kw-and-kwh

@aequitas
Copy link
Contributor Author

Users are reporting metrics stalling when a telegram failed to be parsed. Will be looking into this.

@aequitas
Copy link
Contributor Author

I refactored the component a little bit to remove some complexity. I learned Protocols is the way to go in asyncio, so that is the implementation I went for this time.

from homeassistant.helpers.entity import Entity

DOMAIN = 'dsmr'

REQUIREMENTS = ['dsmr-parser==0.3']
REQUIREMENTS = [
'https://github.com/aequitas/dsmr_parser/archive/async_protocol.zip'
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

for device in devices:
device.telegram = telegram
hass.async_add_job(device.async_update_ha_state)
# hass.loop.create_task(device.async_update_ha_state())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nicely spot, left that in there by accident.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I suggest adding: https://github.com/aequitas/pytest-eradicate to the project?

@balloob
Copy link
Member

balloob commented Nov 23, 2016

Great 🐬

@balloob balloob merged commit 64cfc4f into home-assistant:dev Nov 23, 2016
@aequitas
Copy link
Contributor Author

@balloob thanks.

I had some todo's still unchecked in the PR description. What is the recommended way to indicate a PR should not yet be merged? I though leaving todos unchecked would be a good indicator for that.

Merge should not have to be reverted I am pushing the update in a minute and the error handling seems to only affect one of the test users atm with which I am in debug session on the community forum.

@lwis
Copy link
Member

lwis commented Nov 23, 2016

@aequitas if you create a new PR and tag me I'll review and merge if it all looks good.

If you don't want something to be merged and you're just looking for feedback, it's best to put "WIP" in the title.

@aequitas
Copy link
Contributor Author

@lwis ok, this one was a little in between because at first I considered it ready for merge but then some users had feedback already. I will keep with the WIP convention next time.

@balloob
Copy link
Member

balloob commented Nov 24, 2016

Sorry, missed the to do checkbox. WIP is the best indicator.

@home-assistant home-assistant locked and limited conversation to collaborators Mar 17, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants