-
-
Notifications
You must be signed in to change notification settings - Fork 28.7k
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
DSMR sensor #4309
Conversation
|
||
_LOGGER.info('got new telegram') | ||
|
||
yield from asyncio.sleep(10, loop=self.hass.loop) |
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.
What's this sleep for?
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.
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.
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.
@aequitas you'll be better off using @Throttle
in that case 👍
self._telegram = {} | ||
|
||
@asyncio.coroutine | ||
def async_update(self): |
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.
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.
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.
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).
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.
@aequitas if you're planning on making dsmr-parser
then your current implementation will be appropriate.
REQUIREMENTS = ['dsmr-parser==0.2'] | ||
|
||
CONF_DSMR_VERSION = 'dsmr_version' | ||
DEFAULT_DEVICE = '/dev/ttyUSB0' |
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.
I wouldn't recommend to do that because this will most likely not work on macOS and Windows.
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.
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.
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.
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, |
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.
For serial ports we use port:
for the configuration.
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.
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.
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.
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.
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.
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.
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.
I rename the config to port, let me know if you want me to take initiative on documenting the conventions.
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 |
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.
Should we keep this? Add it to .gitignore if needed.
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.
Sorry, this got included by accident.
hass.loop.create_task(dsmr.read_telegrams(queue)) | ||
|
||
|
||
class DSMR: |
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.
Should this be an interface?
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.
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, {}), |
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.
This should be cached in an update
method.
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.
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', |
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.
Why is this commented?
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.
I still need to refactor the test suite after de code has been refactored to non-blocking. This will be addresses (see todo).
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)) |
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.
async_add_devices
is a coroutine function, no need to wrap it in create_task
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.
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)) |
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.
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.
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.
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, {}), |
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.
It's unclear to me if these calls are doing I/O. They shouldn't (as we're inside async land)
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.
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) |
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.
Nice. Didn't know about monkeypatch.
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.
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
.
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.
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.
- 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
@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. |
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.
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. |
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) |
@henkvdt the 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. |
Users are reporting metrics stalling when a telegram failed to be parsed. Will be looking into this. |
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' |
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
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()) |
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.
What's this for?
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.
Nicely spot, left that in there by accident.
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.
May I suggest adding: https://github.com/aequitas/pytest-eradicate to the project?
Great 🐬 |
@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. |
@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. |
@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. |
Sorry, missed the to do checkbox. WIP is the best indicator. |
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):Checklist:
Todos:
dsmr_parser
contribution merged upstream (DSMR 2.2 ndokter/dsmr_parser#2)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
tox
run successfully.REQUIREMENTS
variable.requirements_all.txt
by runningscript/gen_requirements_all.py
.New files were added to.coveragerc
.