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

Support for (German) EasyMeter Q3D using COM-1 Ethernet Gateway #92

Merged
merged 16 commits into from
Nov 21, 2021

Conversation

Aeroid
Copy link
Contributor

@Aeroid Aeroid commented Nov 12, 2021

I run two EasyMeter Q3D for my main energy incl delivery of solar panel generated energy, the second one is a "private" one just to meter my main consumption by a heat pump. Both have IR interfaces which are bridged to ethernet TCP Port 5000 by COM-1 boxes from co-met.info. Unfortunatly the tcp stream from the main meter includes (padding?) \xFF which throws off the decode(ascii), so I set it to errors="ignore". Hope that doesn't create any other issues.

Copy link
Contributor

@lowdef lowdef left a comment

Choose a reason for hiding this comment

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

Thanks for providing the code changes.
Inserted some questions/comments in this review.

@@ -99,7 +102,7 @@ def connection_made(self, transport):

def data_received(self, data):
"""Add incoming data to buffer."""
data = data.decode('ascii')
data = data.decode('ascii', errors='ignore')
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the right location to solve this?
Is this safe for all other use cases as well?
I see for other use cases / readers we always still use the data.decode('ascii') variant. So this adds inconsistency as well.
Wouldn't it be better to have device specific input cleanups be done, before entering the generic parsing library?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, thought about it as well, but I saw a few identical or similar issues reported that this would fix and other forks solved it this way as well.

How would you imagine this would break things?

This is a quite simple fix.

Maybe additional checks further downstream could drop the telegram if needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know honestly.

But here we have one meter, that is adding a trailing \xFF.
The code is changed to ignore any decode error on the complete stream.
So suddenly it is allowing way more probably really erroneous (while un-decodeable) cases.
That does not sound like a good way of handling this.

What about the following solution; we add a new decode_ascii_telegram method, which:

  1. removes all trailing \xFF data.rstrip(b'\xff')
  2. invokes the data.decode('ascii')

and invoke this in all places where currently data.decode('ascii') is used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See my other comment on the garbage added by the COM-1 boxes.

I would say there is sufficient downstream checks with _find_telegrams to ignore the garbage and with the CRC-checks to check the telegram integrity (if CRC is available). Futhermore the Regexs should also fail in cases where lines are incomplete (w/ or w/o CRC).

Unfortunatly \xff are not the last chars, but it's terminated by CR LF. Thus rstrip won't work.

I'm no expert here, but if I understand decode correctly it's just failing when it can't map a bytecode to target charset. It wouldn't catch any other "errors". So generally the "really erroneous" cases are quite limited, I would think. errors="replace" could be an option to "see" these cases.

Using "latin1" (8-bit) instead "ascii" (7-bit) works fine. Any concerns?

@@ -181,3 +181,19 @@
obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)),
}
}


Q3D = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you provide the specification for the protocol profile this meter is using? (A URL would be fine.)
(Maybe the name prefixes LUXEMBOURG_ is awkward, and we find out this is a more generally used item and can fix the names later on.)

Copy link
Contributor Author

@Aeroid Aeroid Nov 14, 2021

Choose a reason for hiding this comment

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

Here is some documentation I found:
EasyMeter Q3D Manual (german)

  • type code on page 8
  • D0-Specs on page 20

Implementation notes for a Q3D reader

  • telegram analysis starting slide 15

OBIS code table

The latter just calls "1.8.0" / "2.8.0" "positive" / "negative" active energy total.

So for my taste these

  • ELECTRICITY_IMPORTED_TOTAL = r'\d-\d:1.8.0.+?\r\n'
  • LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL = r'\d-\d:1.8.0.+?\r\n' # Total imported energy register (P+)
  • LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL = r'\d-\d:2.8.0.+?\r\n' # Total exported energy register (P-)
  • SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL = r'\d-\d:1.8.0.+?\r\n' # Total imported energy register (P+)
  • SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL = r'\d-\d:2.8.0.+?\r\n' # Total exported energy register (P-)

could all be simplified to

  • ELECTRICITY_IMPORTED_TOTAL = r'\d-\d:1.8.0.+?\r\n'
  • ELECTRICITY_EXPORTED_TOTAL = r'\d-\d:2.8.0.+?\r\n'

But to not break things I would just add ELECTRICITY_EXPORTED_TOTAL and use them in the Q3D definition, instead of the LUXEMBOURG_ ones.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is the last thing we need to do, I think. The problem is that the parser can not distinguish between ELECTRICITY_IMPORTED_TOTAL, LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL, as they are exactly the same regarding the matching pattern used.

Same for LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, ELECTRICITY_EXPORTED_TOTAL.

To unbreak things, we have to implement your simplified proposal. You want to give it a go?

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 do get your point, that the same code has different labels per different specifications. When I saw this for the first time, I tought it was a way to be more specific per each country using the codes slightly differently. But I could be wrong.

Since we excatly give the parser just one spec to run with (e.g. 5L), there is no harm for the parser. He only looks at that 5L spec. As long as ELECTRICITY_IMPORTED_TOTAL, LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL don't show up in one spec, this should be fine. I have also not seen any issues reported on this.

So much to the conceptual part.

What remains is cosmetics to deduplicate the SWEDEN/LUXEMBOURG labels. I would still suggest that I do that in a separate pull request, to track issues on possible downstrem dependecies.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we are really bold, we may even go a bit further and see if we could remove the different specs entirely and unify them. This would remove the need to know which spec you need, remove the parameter/option and just deliver data based on a standard OBIS interpretation.

This would only work if the country/vendor specific codes are optional.

The parser could also "warn" if it discovers labels that are not part of the spec and not drop them, but forward them with "numeric" labels to be picked up downstream or enhancement of our spec here.

It might well be a breaking change, so I would suggest do that in a new branch. Also we would need to have folks at least from Sweden and Luxembourg to commit some time for testing, as I only have access to my two little boxes here.

Again, just to be be clear, these are just thoughts and goes well beyond this pull request.

@lowdef
Copy link
Contributor

lowdef commented Nov 13, 2021

"Both have IR interfaces which are bridged to ethernet TCP Port 5000 by COM-1 boxes from co-met.info"

Could you provide some more details on your setup here, I am curious how this works exactly.
I looked at the co-met.info web site but did not find:

  1. information on COM-1 boxes (what are they?)
  2. and how to bridge them to ethernet (what product?)

@Aeroid
Copy link
Contributor Author

Aeroid commented Nov 13, 2021

Hi,

I bought them a couple of years ago and can't find them on their website anymore either. They connect via optokopplers to the top of the EasyMeter and then allow you to listen on port 5000 tcp to the telegrams with an interval of two to three seconds between updates.

Here is a picture:

20211112_202932.jpg

@lowdef
Copy link
Contributor

lowdef commented Nov 13, 2021

Thanks. That is an interesting setup.

You indicate that only the telegrams from the main meter are padded. Can we assume the padding is done by the meter itself?

@Aeroid
Copy link
Contributor Author

Aeroid commented Nov 13, 2021

I have to guess so, as the gateways are the same. If you want, I could switch them and see.

@lowdef
Copy link
Contributor

lowdef commented Nov 13, 2021

Up to you. I guess it interesting to know, but it doesn't help us further in solving the problem.
Please consider my suggestion regarding the decode method.
I am also open for a better naming.

@Aeroid
Copy link
Contributor Author

Aeroid commented Nov 14, 2021

Okay, I switched the meters connected to the COM-1 boxes. The garbage they add after the telegramm stays the same regardless of the Easymeter they connect to.

After the telegramm end "! (CRC)\r\n", both add a "seconds counter" plus some static data, one with readable chars the other with the \xFF.

As anything beyond the end "! (CRC)\r\n" is outside of scope of IEC 62056-21 Mode D, telegram_buffer._find_telegrams() already does the job of ignoring the garbage added by the COM-1 "transport layer".

Below are two full datagrams received including the crc-less telegram and the COM-1-box-garbage:

TELEGRAM_ESY5Q3DB1024_V304 = ( # Easymeter an Hauptstromzähler
    '/ESY5Q3DB1024 V3.04\r\n'
    '\r\n'
    '1-0:0.0.0*255(0272031312565)\r\n'
    '1-0:1.8.0*255(00052185.7825309*kWh)\r\n'
    '1-0:2.8.0*255(00019949.3221493*kWh)\r\n'
    '1-0:21.7.0*255(000747.85*W)\r\n'
    '1-0:41.7.0*255(000737.28*W)\r\n'
    '1-0:61.7.0*255(000639.73*W)\r\n'
    '1-0:1.7.0*255(002124.86*W)\r\n'
    '1-0:96.5.5*255(80)\r\n'
    '0-0:96.1.255*255(1ESY1313002565)\r\n'
    '!\r\n'
    '  25803103\r\n'
    '\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\r\n'
)

TELEGRAM_ESY5Q3DA1004_V304 = ( # Easymeter an Wärmepumpe
    '/ESY5Q3DA1004 V3.04\r\n'
    '\r\n'
    '1-0:0.0.0*255(1336001560)\r\n'
    '1-0:1.8.0*255(00032549.5061662*kWh)\r\n'
    '1-0:21.7.0*255(000557.29*W)\r\n'
    '1-0:41.7.0*255(000521.62*W)\r\n'
    '1-0:61.7.0*255(000609.30*W)\r\n'
    '1-0:1.7.0*255(001688.21*W)\r\n'
    '1-0:96.5.5*255(80)\r\n'
    '0-0:96.1.255*255(1ESY1336001560)\r\n'
    '!\r\n'
    '  25818685\r\n'
    'DE0000000000000000000000000000003\r\n'
)

@Aeroid
Copy link
Contributor Author

Aeroid commented Nov 14, 2021

Accepting latin1 (8-bit char encoding) from the wire seems acceptable, we just have to make sure that the actual telegram is only defined for ascii (7-bit char encoding).

@Aeroid Aeroid changed the title Support for (German) EasyMeter Q3D Support for (German) EasyMeter Q3D using COM-1 Ethernet Gateway Nov 14, 2021
@ndokter
Copy link
Owner

ndokter commented Nov 19, 2021

@lowdef you know more about this change than i do. Is the current change good and can it be merged?

@lowdef
Copy link
Contributor

lowdef commented Nov 19, 2021

@ndokter i will have a look when i have some free time. Need to think about what is a good approach here. The charm of @Aeroid's solution is that it is very simple. The drawback might be that we have one particular specific ethernet gw specific fix in the generic code in one place only. Will come back on this soon.

@lowdef
Copy link
Contributor

lowdef commented Nov 20, 2021

There is some parts I do not understand with the last changes in the code.
Encoding as latin-1 and decoding as ascii again. etc.

This:

        try:
            # we accepted 8-bit at transport level (e.g. tcp)
            telegram_data = telegram.encode("latin1")
            # we need to ensure 7-bit at telegram level (IEC 646 required in section 5.4 of IEC 62056-21)
            telegram = telegram_data.decode("ascii")
            parsed_telegram = self.telegram_parser.parse(telegram)

I decided to give it a try with the telegrams you provided.
But most of the telegarm is not parsed correctly.

Here is what i tried:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from dsmr_parser import telegram_specifications
from dsmr_parser.objects import Telegram
from dsmr_parser.parsers import TelegramParser

TELEGRAM_ESY5Q3DB1024_V304 = ( # Easymeter an Hauptstromzähler
'/ESY5Q3DB1024 V3.04\r\n'
'\r\n'
'1-0:0.0.0255(0272031312565)\r\n'
'1-0:1.8.0255(00052185.7825309kWh)\r\n'
'1-0:2.8.0255(00019949.3221493kWh)\r\n'
'1-0:21.7.0255(000747.85W)\r\n'
'1-0:41.7.0255(000737.28W)\r\n'
'1-0:61.7.0255(000639.73W)\r\n'
'1-0:1.7.0255(002124.86W)\r\n'
'1-0:96.5.5255(80)\r\n'
'0-0:96.1.255*255(1ESY1313002565)\r\n'
'!\r\n'
' 25803103\r\n'
'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\r\n'
)

TELEGRAM_ESY5Q3DA1004_V304 = ( # Easymeter an Wärmepumpe
'/ESY5Q3DA1004 V3.04\r\n'
'\r\n'
'1-0:0.0.0255(1336001560)\r\n'
'1-0:1.8.0255(00032549.5061662kWh)\r\n'
'1-0:21.7.0255(000557.29W)\r\n'
'1-0:41.7.0255(000521.62W)\r\n'
'1-0:61.7.0255(000609.30W)\r\n'
'1-0:1.7.0255(001688.21W)\r\n'
'1-0:96.5.5255(80)\r\n'
'0-0:96.1.255*255(1ESY1336001560)\r\n'
'!\r\n'
' 25818685\r\n'
'DE0000000000000000000000000000003\r\n'
)

sample = TELEGRAM_ESY5Q3DB1024_V304

telegram_specification = telegram_specifications.Q3D
#telegram_specification['checksum_support'] = False

parser = TelegramParser(telegram_specification)
telegram = Telegram(sample, parser, telegram_specification)

print(telegram)

We get following output:


Q3D_EQUIPMENT_IDENTIFIER: 	 0272031312565	[None]
Q3D_EQUIPMENT_STATE: 	 80	[None]
Q3D_EQUIPMENT_SERIALNUMBER: 	 1ESY1313002565	[None]

ignore line with signature \d-\d:1\.8\.0.+?\r\n, because parsing failed.
Traceback (most recent call last):
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 59, in parse
    telegram[signature] = parser.parse(match.group(0))
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 201, in parse
    return CosemObject(self._parse(line))
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 156, in _parse
    return self._parse_values(values)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 141, in _parse_values
    return [self.value_formats[i].parse(value)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 141, in <listcomp>
    return [self.value_formats[i].parse(value)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 285, in parse
    value = self.coerce_type(value) if value is not None else value
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
ignore line with signature \d-\d:2\.8\.0.+?\r\n, because parsing failed.
Traceback (most recent call last):
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 59, in parse
    telegram[signature] = parser.parse(match.group(0))
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 201, in parse
    return CosemObject(self._parse(line))
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 156, in _parse
    return self._parse_values(values)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 141, in _parse_values
    return [self.value_formats[i].parse(value)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 141, in <listcomp>
    return [self.value_formats[i].parse(value)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 285, in parse
    value = self.coerce_type(value) if value is not None else value
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
ignore line with signature \d-\d:21\.7\.0.+?\r\n, because parsing failed.
Traceback (most recent call last):
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 59, in parse
    telegram[signature] = parser.parse(match.group(0))
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 201, in parse
    return CosemObject(self._parse(line))
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 156, in _parse
    return self._parse_values(values)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 141, in _parse_values
    return [self.value_formats[i].parse(value)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 141, in <listcomp>
    return [self.value_formats[i].parse(value)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 285, in parse
    value = self.coerce_type(value) if value is not None else value
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
ignore line with signature \d-\d:41\.7\.0.+?\r\n, because parsing failed.
Traceback (most recent call last):
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 59, in parse
    telegram[signature] = parser.parse(match.group(0))
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 201, in parse
    return CosemObject(self._parse(line))
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 156, in _parse
    return self._parse_values(values)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 141, in _parse_values
    return [self.value_formats[i].parse(value)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 141, in <listcomp>
    return [self.value_formats[i].parse(value)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 285, in parse
    value = self.coerce_type(value) if value is not None else value
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
ignore line with signature \d-\d:61\.7\.0.+?\r\n, because parsing failed.
Traceback (most recent call last):
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 59, in parse
    telegram[signature] = parser.parse(match.group(0))
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 201, in parse
    return CosemObject(self._parse(line))
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 156, in _parse
    return self._parse_values(values)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 141, in _parse_values
    return [self.value_formats[i].parse(value)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 141, in <listcomp>
    return [self.value_formats[i].parse(value)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 285, in parse
    value = self.coerce_type(value) if value is not None else value
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
ignore line with signature \d-\d:1\.7\.0.+?\r\n, because parsing failed.
Traceback (most recent call last):
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 59, in parse
    telegram[signature] = parser.parse(match.group(0))
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 201, in parse
    return CosemObject(self._parse(line))
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 156, in _parse
    return self._parse_values(values)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 141, in _parse_values
    return [self.value_formats[i].parse(value)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 141, in <listcomp>
    return [self.value_formats[i].parse(value)
  File "/home/hanserik/projects/dsmr_parser/dsmr_parser/parsers.py", line 285, in parse
    value = self.coerce_type(value) if value is not None else value
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]

Process finished with exit code 0

Probably the occurence of 255 in the lines causes this.

So the pull request is not ready for merging yet.

@lowdef
Copy link
Contributor

lowdef commented Nov 20, 2021

Character Encoding

Took a bit of digging to find what shall be the character encoding for all telegrams, the national standards don't spell it out, but refer to EN-IEC 62056-21. EN-IEC 62056-21 is hard to get hold off (the official version is ridiculously expensive.) But I found a version of it on the web: EN-IEC 62056-21.

What is the actual character set used for the data part of the telegram messages according to iec62056-21?

This is spelled out in clause 5.5:

5.5 Character code
Character code according to ISO/IEC 646:1991, international reference version. For local use,
a national replacement code can be used.

ISO 646:1991 IRV (international reference version) is basically the same as US-ASCII. (See also: wikipedia article on ISO/IEC 646, oder hier ein sehr übersichtliche Darstellung übersichtliche Darstellung )

Follows that ASCII is the proper choice.

Use of Latin-1 (ISO-8859-1) decoding instead of ASCII decoding

The first 128 characters of latin-1 are identical with ASCII.

The Latin-1 decoder is the most simple of all decoders: it is in fact 1 to 1 mapping of the data bytes.

It seems harmless if we change the decoding to latin1.

CRC check

Inspection of the CRC check shows that it is working on the full 8 bits, therefore this is also fine.

The CRC Check explicitly checks the part between the beginning '/' and the '!'.
So a flipping the most signficant bit would probably be noticed by the CRC check.

The parser

In fact the parser does not limit itself to the the part between the beginning '/' and the '!'.

Meaning that we could add telegram lines after the checksum, which still would be processed or worse override telegram lines from the verified part. This is not good.

I.e.no check here or elsewhere in the stages before that:

        for signature, parser in self.telegram_specification['objects'].items():
            match = re.search(signature, telegram_data, re.DOTALL)

Making it more robust

So to make things more robust we need to drop everything that comes after the checksum.

My conclusion is that it would be:

  • OK to decode the incoming data as latin-1
  • CRC check and all would also take into account the 8th bit
  • we need to ensure that anything past the CRC line is ditched before it is entered into the parsing stages, otherwise strange things may happen.

@Aeroid
Copy link
Contributor Author

Aeroid commented Nov 20, 2021

Thanks for checking. Wondering why its actually running fine on my end for over a week in HomeAssistant with a few lines of changes to the integration to use the Q3D parser.

@lowdef
Copy link
Contributor

lowdef commented Nov 20, 2021

Maybe the 255 thingy is not there in your setup with Home Assistant. Some copy paste error?

@Aeroid
Copy link
Contributor Author

Aeroid commented Nov 20, 2021

Yes, your tests fail here as well. But this works: (reformatted)

$ /usr/local/bin/dsmr_console --version Q3D --verbose --port 5000 --host 192.168.180.98

DEBUG:asyncio:Using selector: EpollSelector
DEBUG:dsmr_parser.clients.protocol:connected
DEBUG:dsmr_parser.clients.protocol:received data: /ESY5Q3DA1004 V3.04

1-0:0.0.0*255(1336001560)
1-0:1.8.0*255(00032748.8569075*kWh)
1-0:21.7.0*255(000051.31*W)
1-0:41.7.0*255(000000.00*W)
1-0:61.7.0*255(000000.00*W)
1-0:1.7.0*255(000051.31*W)
1-0:96.5.5*255(80)
0-0:96.1.255*255(1ESY1336001560)
!
1260284346
DE0000000000000000000000000000003

DEBUG:dsmr_parser.clients.protocol:got telegram: /ESY5Q3DA1004 V3.04

1-0:0.0.0*255(1336001560)
1-0:1.8.0*255(00032748.8569075*kWh)
1-0:21.7.0*255(000051.31*W)
1-0:41.7.0*255(000000.00*W)
1-0:61.7.0*255(000000.00*W)
1-0:1.7.0*255(000051.31*W)
1-0:96.5.5*255(80)
0-0:96.1.255*255(1ESY1336001560)
!

1336001560 None
32748.8569075 kWh
51.31 W
0.00 W
0.00 W
51.31 W
80 None
1ESY1336001560 None

$ /usr/local/bin/dsmr_console --version Q3D --verbose --port 5000 --host 192.168.180.50
DEBUG:asyncio:Using selector: EpollSelector
DEBUG:dsmr_parser.clients.protocol:connected
DEBUG:dsmr_parser.clients.protocol:received data: /ESY5Q3DB1024 V3.04

1-0:0.0.0*255(0272031312565)
1-0:1.8.0*255(00052530.0732483*kWh)
1-0:2.8.0*255(00019961.2853382*kWh)
1-0:21.7.0*255(000217.41*W)
1-0:41.7.0*255(000339.23*W)
1-0:61.7.0*255(000085.97*W)
1-0:1.7.0*255(000642.61*W)
1-0:96.5.5*255(80)
0-0:96.1.255*255(1ESY1313002565)
!
  26916743
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ

DEBUG:dsmr_parser.clients.protocol:got telegram: /ESY5Q3DB1024 V3.04

1-0:0.0.0*255(0272031312565)
1-0:1.8.0*255(00052530.0732483*kWh)
1-0:2.8.0*255(00019961.2853382*kWh)
1-0:21.7.0*255(000217.41*W)
1-0:41.7.0*255(000339.23*W)
1-0:61.7.0*255(000085.97*W)
1-0:1.7.0*255(000642.61*W)
1-0:96.5.5*255(80)
0-0:96.1.255*255(1ESY1313002565)
!

0272031312565 None
52530.0732483 kWh
19961.2853382 kWh
217.41 W
339.23 W
85.97 W
642.61 W
80 None
1ESY1313002565 None

@lowdef
Copy link
Contributor

lowdef commented Nov 20, 2021

I found your samples under example_telegrams.py. They work allright.
Looks like the '*' characters fell out when you copied it to this Thread.

@Aeroid
Copy link
Contributor Author

Aeroid commented Nov 20, 2021

Yes, sorry formatting messed it up ... see above

@Aeroid
Copy link
Contributor Author

Aeroid commented Nov 20, 2021

Same issue in your tests. Now they work.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from dsmr_parser import telegram_specifications
from dsmr_parser.objects import Telegram
from dsmr_parser.parsers import TelegramParser
from example_telegrams import TELEGRAM_ESY5Q3DB1024_V304 , TELEGRAM_ESY5Q3DA1004_V304

sample = TELEGRAM_ESY5Q3DB1024_V304

telegram_specification = telegram_specifications.Q3D
#telegram_specification['checksum_support'] = False

parser = TelegramParser(telegram_specification)
telegram = Telegram(sample, parser, telegram_specification)

print(telegram)

runs like a charm

$ python3 test_Q3D.py
got telegram: %s /ESY5Q3DB1024 V3.04

1-0:0.0.0*255(0272031312565)
1-0:1.8.0*255(00052185.7825309*kWh)
1-0:2.8.0*255(00019949.3221493*kWh)
1-0:21.7.0*255(000747.85*W)
1-0:41.7.0*255(000737.28*W)
1-0:61.7.0*255(000639.73*W)
1-0:1.7.0*255(002124.86*W)
1-0:96.5.5*255(80)
0-0:96.1.255*255(1ESY1313002565)
!

{'\\d-\\d:0\\.0\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x7fb0fcdbaf70>, '\\d-\\d:1\\.8\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x7fb0fcfe6bb0>, '\\d-\\d:2\\.8\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x7fb0fcdbcf40>, '\\d-\\d:21\\.7\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x7fb0fcdbc2e0>, '\\d-\\d:41\\.7\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x7fb0fcdbc070>, '\\d-\\d:61\\.7\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x7fb0fc6b63a0>, '\\d-\\d:1\\.7\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x7fb0fc6b6430>, '\\d-\\d:96\\.5\\.5.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x7fb0fc6b64c0>, '\\d-\\d:96\\.1\\.255.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x7fb0fc6b64f0>}

@Aeroid
Copy link
Contributor Author

Aeroid commented Nov 20, 2021

re "that anything past the CRC line is ditched before it is entered into the parsing stages"

telegram_buffer.get_all() does excatly that before protocol.py hands it over to handle_telegram where the parser is actually called.

Don't you think thats sufficient?

@Aeroid
Copy link
Contributor Author

Aeroid commented Nov 20, 2021

FYI Here is a deeper look inside of the COM-1 box which is actually a WIZnet WIZ110SR. 20211115_203053.jpg

@lowdef
Copy link
Contributor

lowdef commented Nov 20, 2021

telegram_buffer.get_all() does exactly that before protocol.py hands it over to handle_telegram where the parser is actually called.

Yes, you are right. its a long time ago I looked at that part.

I think that this in handle_telegram does not really add value then:

            # we accepted 8-bit at transport level (e.g. tcp)
            telegram_data = telegram.encode("latin1")
            # we need to ensure 7-bit at telegram level (IEC 646 required in section 5.4 of IEC 62056-21)
            telegram = telegram_data.decode("ascii")

so I propose to remove it.

@lowdef
Copy link
Contributor

lowdef commented Nov 20, 2021

WIZnet WIZ110SR.
Interesting. Looks like it is still for sale ;-)

Copy link
Contributor

@lowdef lowdef left a comment

Choose a reason for hiding this comment

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

nearly there, see 2 last requested changes:

  • remove unneccessary addition from handle_telegram
  • consolidate the overlapping OBIS codes

@@ -181,3 +181,19 @@
obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)),
}
}


Q3D = {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the last thing we need to do, I think. The problem is that the parser can not distinguish between ELECTRICITY_IMPORTED_TOTAL, LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL, as they are exactly the same regarding the matching pattern used.

Same for LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, ELECTRICITY_EXPORTED_TOTAL.

To unbreak things, we have to implement your simplified proposal. You want to give it a go?

@Aeroid
Copy link
Contributor Author

Aeroid commented Nov 21, 2021

With this I had just deferred decoding to 7-bit from the receiving to the parsing stage, allowing 8-bit just when we receive data from the port. At this stage we only handle the telegram and prepare for parsing it. Decoding to 7-bit in handle_telegram will throw exceptions right away when the actual telegram doesn't comply to spec. Otherwise we are leaving it up to the parser and crc-check to find such errors. In case of Q3D we don't have a crc. Also this limits my pull request to just allow 8-bit on the transport layer surrounding 7-bit telegram, where the \xFF-issues are located.

I would suggest to keep it.

I'm happy to consolidate the obis codes, but would suggest to do that in a second pull request, as it will affect 5S and 5L.

Are you fine with this and ready to merge?

@lowdef
Copy link
Contributor

lowdef commented Nov 21, 2021

Fair enough.

Can we then agree to the change below?

  • keeps the impact completely localized in data_receive().
  • keeps handle_telegram() uncluttered and readable.

Could not push it to the Pull Request, so here is a diff:

hanserik@famsrv1:~/projects/dsmr_parser$ git diff 26ac27c347c39d473c7f034eab2bdb295927d2df a04a7cc4c5974107b482cce55f48dd6c5e32d7f8
diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py
index c2d032f..ca2b4eb 100644
--- a/dsmr_parser/clients/protocol.py
+++ b/dsmr_parser/clients/protocol.py
@@ -102,12 +102,16 @@ class DSMRProtocol(asyncio.Protocol):
 
     def data_received(self, data):
         """Add incoming data to buffer."""
+
+        # accept latin-1 on the line, to allow for non-ascii padding
         data = data.decode("latin1")
         self._active = True
         self.log.debug('received data: %s', data)
         self.telegram_buffer.append(data)
 
         for telegram in self.telegram_buffer.get_all():
+            # ensure actual telegram is ascii only
+            telegram = telegram.encode("latin1").decode("ascii")
             self.handle_telegram(telegram)
 
     def keep_alive(self):
@@ -134,10 +138,6 @@ class DSMRProtocol(asyncio.Protocol):
         self.log.debug('got telegram: %s', telegram)
 
         try:
-            # we accepted 8-bit at transport level (e.g. tcp)
-            telegram_data = telegram.encode("latin1")
-            # we need to ensure 7-bit at telegram level (IEC 646 required in section 5.4 of IEC 62056-21)
-            telegram = telegram_data.decode("ascii")
             parsed_telegram = self.telegram_parser.parse(telegram)
         except InvalidChecksumError as e:
             self.log.warning(str(e))

@lowdef
Copy link
Contributor

lowdef commented Nov 21, 2021

@ndokter, looks good now. Can be merged.

@Aeroid
Copy link
Contributor Author

Aeroid commented Nov 21, 2021

Would be nice also to bump up the version to 0.31 subsequently. But not sure what's needed for a release. ... I'm a github novice

@lowdef
Copy link
Contributor

lowdef commented Nov 21, 2021

Leave that up to @ndokter. He is the master of this repo. We are only humble servants ;-)

@ndokter ndokter merged commit 00248f4 into ndokter:master Nov 21, 2021
@ndokter
Copy link
Owner

ndokter commented Nov 21, 2021

Heh i wish it was a standalone repo with multiple people having access. Just didnt find the motivation to set it up yet :) Thanks for your work, ive merged it and released it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants