Dsmr-reader: Read data from ESP8266 network socket?

Created on 24 Dec 2016  路  64Comments  路  Source: dsmrreader/dsmr-reader

Software looks good!

However, i would like to run it inside a VM on my homeserver, which isn't near my smartmete and thus no serial tty port. Would it be possible to include support for reading P1-data from a network socket? I already found this ESP8266 project: http://romix.macuser.nl/software.html and fiddled around with it. With Putty i can receive telegrams, so that part works.

Most helpful comment

Ik heb de dsmr_datalogger uitgeschakeld in supervisor en een eigen supervisor config gemaakt voor het Python-script wat je hebt gemaakt om de ESP8266 uit te lezen. Voor iemand die dit in de toekomst leest en ook ge茂nteresseerd is om de P1-poort van de slimme meter draadloos uit te lezen, ik heb het hele spulletje (incl. firmware voor ESP8266) hier online gezet.

Dennis, bedankt voor je hulp om dit werkend te krijgen!

All 64 comments

Hi, thank you for your question.

As a matter of fact, there is API support for posting the readings remotely to the app. By coincidence, I just created #185 with an example script.
The only thing is, I do not have any experience with ESP devices, but it should be possible I guess. Any program able to read the network socket, and create a HTTP POST request, should do the trick.

Can you provide me with an example of how you read the network socket with Putty? I might be able to replace the serial input, of the script in #185, with reading data from a network socket.

I already fiddled around with some code and with this piece of Python-code, i can read one telegram and then it returns back to the command prompt:

import socket

# creates socket object
s = socket.socket(socket.AF_INET,
                  socket.SOCK_STREAM)

host = '192.168.4.17'
port = 8088 

s.connect((host, port))

telegram = s.recv(1024) # msg can only be 1024 bytes long

#s.close()
print(telegram)

But my Python skills are reeeally basic, so it is getting too complex for me to integrate this into your project.

By the way, i couldn't find it quickly, but does your code verify if the CRC that is sent along with the telegram matches with a checksum that is calculated over the telegram?

Hi, the CRC verification is lacking, but I think that's a good idea anyway, so I'll create an separate issue for it. I am planning to use another parser in #154, but CRC lacks there as well (for the moment).

As for the sockets, it's a bit hard scripting remotely, but can you try whether this works?

There are some debugging print, but when succesful, it should print ----- Read telegram ----- followed by the telegram.

from time import sleep
import socket

HOST = '192.168.4.17'
PORT = 8088


def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))

    while True:
        telegram = read_telegram(s)
        print('----- Read telegram -----')
        print(telegram)


def read_telegram(s):
    """ Reads the socket until we can create a reading point. """

    telegram_start_seen = False
    telegram = ''

    # Just keep fetching data until we got what we were looking for.
    while True:
        print('Listening at socket...')
        socket_data = s.recv(1024)
        print('RECV:', socket_data)
        sleep(1)

        for data in socket_data.split("\n"):
            print("DATA LINE:", data)
            try:
                # Make sure weird characters are converted properly.
                data = str(data, 'utf-8')
            except TypeError:
                pass

            # This guarantees we will only parse complete telegrams. (issue #74)
            if data.startswith('/'):
                telegram_start_seen = True

            # Delay any logging until we've seen the start of a telegram.
            if telegram_start_seen:
                telegram += data

            # Telegrams ends with '!' AND we saw the start. We should have a complete telegram now.
            if data.startswith('!') and telegram_start_seen:
                return telegram


if __name__ == '__main__':
    main()

Thank you, i will try that somewhere in the following days :)

Heb je dit nog kunnen testen of is dit niet meer van toepassing sinds de CRC-check?

Nee, helaas nog niet aan toe gekomen. Het staat nog wel op de planning, moet komende week vast wel ergens lukken.

Het is zeker nog van toepassing. Er ligt nu een 5m. lange kabel van mijn meterkast naar m'n server om de P1-poort uit te lezen. Als ik die kabel kan vervangen door een draadloze oplossing dan heeft dat zeker mijn voorkeur. Dit issue heeft overigens geen haast hoor.

Ik had eindelijk wat tijd om hier weer naar te kijken. Een RJ11-splitter op de P1-poort gezet zodat dsmr-reader gewoon door kan blijven gaan met loggen en op de tweede uitgang de ESP8266 geknupt. Script wat je hierboven plaatste even geprobeerd, dit is de output:

domotica@domotica-vm:~$ python /home/domotica/p1smartmeter/esp.py
Listening at socket...
('RECV:', '/KFM5KAIFA-METER\r\n\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(170104195652W)\r\n0-0:96.1.1(SERIAL_OF_MY_METER_HERE)\r\n1-0:1.8.1(002015.856*kWh)\r\n1-0:1.8.2(002048.232*kWh)\r\n1-0:2.8.1(000000.000*kWh)\r\n1-0:2.8.2(000000.000*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.379*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00005)\r\n0-0:96.7.9(00003)\r\n1-0:99.97.0(1)(0-0:96.7.19)(000101000001W)(2147483647*s)\r\n1-0:32.32.0(00000)\r\n1-0:32.36.0(00000)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n1-0:31.7.0(001*A)\r\n1-0:21.7.0(00.379*kW)\r\n1-0:22.7.0(00.000*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(SERIAL_OF_MY_METER_HERE)\r\n0-1:24.2.1(170104190000W)(01539.233*m3)\r\n!DA5E\x00\r\n')
('DATA LINE:', '/KFM5KAIFA-METER\r')
('DATA LINE:', '\r')
('DATA LINE:', '1-3:0.2.8(42)\r')
('DATA LINE:', '0-0:1.0.0(170104195652W)\r')
('DATA LINE:', '0-0:96.1.1(SERIAL_OF_MY_METER_HERE)\r')
('DATA LINE:', '1-0:1.8.1(002015.856*kWh)\r')
('DATA LINE:', '1-0:1.8.2(002048.232*kWh)\r')
('DATA LINE:', '1-0:2.8.1(000000.000*kWh)\r')
('DATA LINE:', '1-0:2.8.2(000000.000*kWh)\r')
('DATA LINE:', '0-0:96.14.0(0002)\r')
('DATA LINE:', '1-0:1.7.0(00.379*kW)\r')
('DATA LINE:', '1-0:2.7.0(00.000*kW)\r')
('DATA LINE:', '0-0:96.7.21(00005)\r')
('DATA LINE:', '0-0:96.7.9(00003)\r')
('DATA LINE:', '1-0:99.97.0(1)(0-0:96.7.19)(000101000001W)(2147483647*s)\r')
('DATA LINE:', '1-0:32.32.0(00000)\r')
('DATA LINE:', '1-0:32.36.0(00000)\r')
('DATA LINE:', '0-0:96.13.1()\r')
('DATA LINE:', '0-0:96.13.0()\r')
('DATA LINE:', '1-0:31.7.0(001*A)\r')
('DATA LINE:', '1-0:21.7.0(00.379*kW)\r')
('DATA LINE:', '1-0:22.7.0(00.000*kW)\r')
('DATA LINE:', '0-1:24.1.0(003)\r')
('DATA LINE:', '0-1:96.1.0(SERIAL_OF_MY_METER_HERE)\r')
('DATA LINE:', '0-1:24.2.1(170104190000W)(01539.233*m3)\r')
('DATA LINE:', '!DA5E\x00\r')
----- Read telegram -----
!DA5E4.2.1(170104190000W)(01539.233*m3)383134)7483647*s)
Listening at socket...

En dan na Listening at socket begint bovenstaande weer opnieuw. Volgens mij zijn we al een heel eind dan, of niet?

Ik hoop dat je hier iets mee kunt. Ik laat de ESP8266 eraan hangen, mocht je een ander script hebben dan kan ik dat eenvoudig opnieuw testen dan.

Mooi, ik had niet verwacht dat die meteen zo ver zou komen.

Hopelijk gaat deze dan ook in 1 x goed, let even goed op de stappen onderaan het codeblok, een variant op deze:

from time import sleep
import socket

import requests

HOST = '192.168.4.17'
PORT = 8088


API_SERVERS = (
    ('http://HOST-OR-IP/api/v1/datalogger/dsmrreading', 'api-key'),
)


def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))

    while True:
        telegram = read_telegram(s)
        print('Read telegram:')
        print(telegram)

        for current_server in API_SERVERS:
            api_url, api_key = current_server
            send_telegram(telegram, api_url, api_key)
            print('Sent telegram to:', api_url)

        sleep(1)


def read_telegram(s):
    """ Reads the socket until we can create a reading point. """

    telegram_start_seen = False
    telegram = ''

    # Just keep fetching data until we got what we were looking for.
    while True:
        print('Listening at socket...')
        socket_data = s.recv(1024)

        for data in socket_data.split("\n"):
            try:
                # Make sure weird characters are converted properly.
                data = str(data, 'utf-8')
            except TypeError:
                pass

            # This guarantees we will only parse complete telegrams. (issue #74)
            if data.startswith('/'):
                telegram_start_seen = True

            # Delay any logging until we've seen the start of a telegram.
            if telegram_start_seen:
                telegram += data

            # Telegrams ends with '!' AND we saw the start. We should have a complete telegram now.
            if data.startswith('!') and telegram_start_seen:
                return telegram


def send_telegram(telegram, api_url, api_key):
    # Register telegram by simply sending it to the application with a POST request.
    response = requests.post(
        api_url,
        headers={'X-AUTHKEY': api_key},
        data={'telegram': telegram},
    )

    # You will receive a status 200/201 when successful.
    if response.status_code not in (200, 201):
        # Or you will find the error (hint) in the response body on failure.
        print('[!] Error: {}'.format(response.text))


if __name__ == '__main__':
    main()

Stappen v贸贸r uitvoer:

  • In bovenstaand script bij API_SERVERS de host van waar je DSMR-reader draait invullen.
  • Hiervoor zul je in DSMR-reader bij de configuratie de API moeten inschakelen.
  • En tevens de random API key die daar staat (of een eigen variant) kopieren naar bovenstaand script, ook in API_SERVERS.
  • Ervoor zorgen dat je de requests library installeert op de plek waar je dit script uitvoert (pip install requests)
  • Ik weet niet hoe de applicatie omgaat met dubbele readings, dus wellicht even tijdelijk je andere datalogger in DSMR-reader stoppen.

Gaat nog niet helemaal goed:

domotica@domotica-vm:~$ python /home/domotica/p1smartmeter/esp.py
Listening at socket...
Read telegram:
!4C194.2.1(170104210000W)(01540.161*m3)383134)7483647*s)
[!] Error: Failed to parse telegram
('Sent telegram to:', 'http://192.168.4.21/api/v1/datalogger/dsmrreading')
Listening at socket...
Read telegram:
!C9634.2.1(170104210000W)(01540.161*m3)383134)7483647*s)
[!] Error: Failed to parse telegram

Dit gaat de hele tijd zo door, er wordt niet 茅茅n keer een telegram succesvol verzonden naar dsmr-reader.

In het script heb ik de connectie als volgt ingevuld (api-key wat ingekort):

API_SERVERS = (
    ('http://192.168.4.21/api/v1/datalogger/dsmrreading', '5S7SVP.......PO6ZJB'),
)

Dat klopt? De foutmelding Failed to parse telegram lijkt mij afkomstig van de API, omdat ik hem in het script zelf nergens terugvind.

Komt dit door dat er wellicht een carriage return / linefeed wordt meegestuurd (of juist niet)?

Ja, volgens mij zie ik het al en klopt het wat je al vermoedt.

Ik splits het bericht per linefeed, want die krijg ik normaal terug via de seriele poort, maar ik zet die niet terug bij elke regel.

Dus wellicht kun je van de regel:

telegram += data

Dit maken:

telegram += data + "\n"

Jij bent echt de held van de dag! Aanpassing gedaan en het lijkt nu perfect te werken! 馃憤 馃憤 馃憤

domotica@domotica-vm:~$ python /home/domotica/p1smartmeter/esp.py
Listening at socket...
Read telegram:
/KFM5KAIFA-METER

1-3:0.2.8(42)
0-0:1.0.0(170104223828W)
0-0:96.1.1(SERIALNUMBERHERE)
1-0:1.8.1(002015.856*kWh)
1-0:1.8.2(002049.345*kWh)
1-0:2.8.1(000000.000*kWh)
1-0:2.8.2(000000.000*kWh)
0-0:96.14.0(0002)
1-0:1.7.0(00.258*kW)
1-0:2.7.0(00.000*kW)
0-0:96.7.21(00005)
0-0:96.7.9(00003)
1-0:99.97.0(1)(0-0:96.7.19)(000101000001W)(2147483647*s)
1-0:32.32.0(00000)
1-0:32.36.0(00000)
0-0:96.13.1()
0-0:96.13.0()
1-0:31.7.0(001*A)
1-0:21.7.0(00.257*kW)
1-0:22.7.0(00.000*kW)
0-1:24.1.0(003)
0-1:96.1.0(....)
0-1:24.2.1(170104220000W)(01540.272*m3)
!671D

('Sent telegram to:', 'http://192.168.4.21/api/v1/datalogger/dsmrreading')
Listening at socket...

Het proces dsmr_datalogger in supervisor zorgt normaal voor het uitlezen van de ttyUSB0 toch? Als ik die stopzet en snel even een nieuwe supervisor config maak voor dit Python dingetje, dan zou alles net zo moeten werken als met een fysieke kabel toch? Dan laat ik hem vannacht namelijk even draaien zodat ik morgenochtend een update kan geven. Hoe kan ik rechtstreeks de database querien? Dat gaat wat sneller dan dat ik 100x moet klikken om te kijken of hij ook waardes heeft gemist.

Edit: Ik probeer het op de VM waarin ik dsmr-reader heb draaien, uit te voeren maar dan krijg ik telkens deze foutmelding:

(dsmrreader) dsmr@dsmrreader-vm:~$ python /home/dsmr/dsmr-reader/esp_sockets.py
Listening at socket...
Traceback (most recent call last):
  File "/home/dsmr/dsmr-reader/esp_sockets.py", line 78, in <module>
    main()
  File "/home/dsmr/dsmr-reader/esp_sockets.py", line 20, in main
    telegram = read_telegram(s)
  File "/home/dsmr/dsmr-reader/esp_sockets.py", line 43, in read_telegram
    for data in socket_data.split("\n"):
TypeError: a bytes-like object is required, not 'str'
(dsmrreader) dsmr@dsmrreader-vm:~$

In die andere VM waar ik het net mee probeerde werkte het wel, dus er zal iets verschillend zijn, maar wat?

  • Die fout komt doordat de code het wel doet in Python 2, maar de omgeving van dsmr-reader Python 3 heeft. Dit heeft te maken met een van de grootste verschillen onderling, namelijk de afhandeling van strings. Kun je kijken of dit werkt? telegram += data + "\n" wordt telegram += data + b"\n" (zie de extra b die direct v贸贸r en tegen de string moet staan)

  • De database kun je als sudo/root gebruiker zo makkelijk bekijken (laatste 1000 metingen aflopend):

sudo sudo -u postgres psql dsmrreader -c "select id, "timestamp" from dsmr_datalogger_dsmrreading order by id desc limit 1000;"

Helaas, heb het aangepast met die b erin (jouw code gekopieerd) maar krijg nog steeds een foutmelding:

(dsmrreader) dsmr@dsmrreader-vm:~$ python /home/dsmr/dsmr-reader/esp_sockets.py
Listening at socket...
Traceback (most recent call last):
  File "/home/dsmr/dsmr-reader/esp_sockets.py", line 78, in <module>
    main()
  File "/home/dsmr/dsmr-reader/esp_sockets.py", line 20, in main
    telegram = read_telegram(s)
  File "/home/dsmr/dsmr-reader/esp_sockets.py", line 43, in read_telegram
    for data in socket_data.split("\n"):
TypeError: a bytes-like object is required, not 'str'

Ik heb het op die andere VM in een supervisor gegooid en laat het vannacht draaien. Het in deze VM werkend krijgen komt wel. Ik ben veel meer benieuwd naar hoe stabiel zich dit verhoud als het een aantal uren draait. Het script doet geen check op CRC zie ik, doet de API dat wel? Het voordeel in dit geval is dat de code op de ESP8266 ook al een check op de CRC doet voordat hij het het netwerk op duwt. Maar tussen ESP en Python-script kan het dus nog misgaan en wellicht zijn er ook mensen die de meter gaan uitlezen met iets wat geen CRC check doet.

Ik ga nu slapen, ik laat het morgen weten hoe het vannacht heeft gedraaid. We komen er wel samen!馃槑

Ik ben niet helemaal wakker zie ik. Je foutmeldingen geven een compleet ander stuk code aan als oorzaak, d貌h. Alleen wellicht dat de b op die plek uiteindelijk ook nodig was.

Kun je hetzelfde truukje halen bij for data in socket_data.split("\n"):? Dus dat wordt for data in socket_data.split(b"\n"):
Het is jammer dat ik niet exact dezelfde setup heb hier, dan kan ik je direct werkende code geven mja.

Wat betreft de CRC-check, die wordt op het laatst moment gedaan, vlak voordat de meting wordt weggeschreven in de database. Feitelijk doet de datalogger nu ook al geen CRC-check, maar gebeurt dat indirect in de achterliggende code.

dsmr_datalogger.services.telegram_to_reading(...) (CRC):

Helaas, ook dan weer een foutmelding:

(dsmrreader) dsmr@dsmrreader-vm:~$ python /home/dsmr/dsmr-reader/esp_sockets.py                                         Listening at socket...
Traceback (most recent call last):
  File "/home/dsmr/dsmr-reader/esp_sockets.py", line 78, in <module>
    main()
  File "/home/dsmr/dsmr-reader/esp_sockets.py", line 20, in main
    telegram = read_telegram(s)
  File "/home/dsmr/dsmr-reader/esp_sockets.py", line 56, in read_telegram
    telegram += data + b"\n"
TypeError: Can't convert 'bytes' object to str implicitly
(dsmrreader) dsmr@dsmrreader-vm:~$

Voor de volledigheid hier nog even het gehele script waarmee ik bovenstaande error krijg:

from time import sleep
import socket

import requests

HOST = '192.168.4.17'
PORT = 8088


API_SERVERS = (
    ('http://192.168.4.21/api/v1/datalogger/dsmrreading', '5S7SV....JB'),
)


def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))

    while True:
        telegram = read_telegram(s)
        print('Read telegram:')
        print(telegram)

        for current_server in API_SERVERS:
            api_url, api_key = current_server
            send_telegram(telegram, api_url, api_key)
            print('Sent telegram to:', api_url)

        sleep(1)


def read_telegram(s):
    """ Reads the socket until we can create a reading point. """

    telegram_start_seen = False
    telegram = ''

    # Just keep fetching data until we got what we were looking for.
    while True:
        print('Listening at socket...')
        socket_data = s.recv(1024)

        for data in socket_data.split(b"\n"):
            try:
                # Make sure weird characters are converted properly.
                data = str(data, 'utf-8')
            except TypeError:
                pass

            # This guarantees we will only parse complete telegrams. (issue #74)
            if data.startswith('/'):
                telegram_start_seen = True

            # Delay any logging until we've seen the start of a telegram.
            if telegram_start_seen:
                telegram += data + b"\n"

            # Telegrams ends with '!' AND we saw the start. We should have a complete telegram now.
            if data.startswith('!') and telegram_start_seen:
                return telegram


def send_telegram(telegram, api_url, api_key):
    # Register telegram by simply sending it to the application with a POST request.
    response = requests.post(
        api_url,
        headers={'X-AUTHKEY': api_key},
        data={'telegram': telegram},
    )

    # You will receive a status 200/201 when successful.
    if response.status_code not in (200, 201):
        # Or you will find the error (hint) in the response body on failure.
        print('[!] Error: {}'.format(response.text))


if __name__ == '__main__':
    main()

Zoals gezegd heb ik het in die andere VM wel draaiend, kan ik daar mooi kijken of het blijft draaien en kun jij rustig kijken of je nog een idee hebt. Nu echt slapen 馃槈

Wellicht is dan de oplossing om de b in de laatste telegram += data + b"\n" weer weg te halen. En dan alleen degene te behouden die als tweede is toegevoegd in de split().


Ik kan het toch soort van lokaal reproduceren op de shell en ik krijg voor de drie varianten ook exact dezelfde foutmeldingen:

>>> [str(x, 'utf-8') + "\n" for x in b'abcde\nfghij\nklnmop'.split("\n")]

TypeError: a bytes-like object is required, not 'str'

Daarna de b voor de + "\n":

>>> [str(x, 'utf-8') + b"\n" for x in b'abcde\nfghij\nklnmop'.split("\n")]

TypeError: a bytes-like object is required, not 'str'

Daarna de b voor de split("\n"):

>>> [str(x, 'utf-8') + b"\n" for x in b'abcde\nfghij\nklnmop'.split(b"\n")]

TypeError: Can't convert 'bytes' object to str implicitly

Uiteindelijk de eerst toegevoegde b weghalen (gaat goed):

>>> [str(x, 'utf-8') + "\n" for x in b'abcde\nfghij\nklnmop'.split(b"\n")]

['abcde\n', 'fghij\n', 'klnmop\n']

Ben benieuwd :]

Het script is netjes blijven draaien vannacht:

supervisor> status
p1esp8266                        RUNNING   pid 19614, uptime 8:34:55

Daarna in de database gekeken op de dsmr-reader VM en hij lijkt continu te hebben gelogd, nice!

Aanpassing gedaan in het script op de dsmr-reader VM zoals jij zegt en dan knalt het script er niet meer uit, maar krijg ik wel foutmeldingen:

(dsmrreader) dsmr@dsmrreader-vm:~/dsmr-reader$ python /home/dsmr/dsmr-reader/esp_sockets.py
Listening at socket...
Read telegram:
!ECBD4.2.1(170105080000W)(01540.862*m3)383134)7483647*s)
[!] Error: Failed to parse telegram
Sent telegram to: http://192.168.4.21/api/v1/datalogger/dsmrreading
Listening at socket...
Read telegram:
!F0144.2.1(170105080000W)(01540.862*m3)383134)7483647*s)
[!] Error: Failed to parse telegram
Sent telegram to: http://192.168.4.21/api/v1/datalogger/dsmrreading

Zie je onder Read telegram: wel een compleet telegram staan? Of slechts een deel zoals je letterlijk hierboven post?

De output hierboven is wat ik zie, heb niks eruit geknipt.

Okee, dan gaat er nu weer iets mis met het samenvoegen. Dit zie ik al terug in een eerder bericht. Ik denk dat je dan even moet kijken of het splitsen op de linefeed goed gaat.

Maar wellicht eerst even iets anders proberen. Ik zie namelijk dat hier recv() een compleet telegram teruggeeft. Je zou kunnen proberen om die data compleet op te sturen en te hopen dat de recv() alles in 1x opgehaald heeft.

Script is ook even ingekort zonder comments enzo. Je kunt alles onder API_SERVERS vervangen met:

def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))

    while True:
        socket_data = s.recv(1024)
        telegram = str(socket_data, 'utf-8')

        print('Read telegram:')
        print(telegram)

        for current_server in API_SERVERS:
            api_url, api_key = current_server
            print('Sending telegram to:', api_url)
            send_telegram(telegram, api_url, api_key)

        sleep(1)


def send_telegram(telegram, api_url, api_key):
    response = requests.post(
        api_url,
        headers={'X-AUTHKEY': api_key},
        data={'telegram': telegram},
    )

    if response.status_code not in (200, 201):
        print('[!] API error: {}'.format(response.text))


if __name__ == '__main__':
    main()

Wellicht werkt dit meteen? read_telegram() is dus flink ingekort.

Dat werkt meteen idd!

(dsmrreader) dsmr@dsmrreader-vm:~/dsmr-reader$ python /home/dsmr/dsmr-reader/esp_sockets.py
Listening at socket...
Read telegram:
/KFM5KAIFA-METER

1-3:0.2.8(42)
0-0:1.0.0(170105202122W)
...knip, rest van telegram...
0-1:96.1.0(47......34)
0-1:24.2.1(170105200000W)(01544.140*m3)
!0720

Sending telegram to: http://192.168.4.21/api/v1/datalogger/dsmrreading
Listening at socket...

馃憤

Trouwens, volgens mij doet deze code geen foutafhandeling toch? Stel dat m'n wifi even hikt, dan gaat het script onderuit denk ik?

Top, mooi man. Waarschijnlijk kan die while True: er dan ook uit, want hij doet toch al meteen een return na de eerste recv().

  • Ik heb mijn vorige comment bijgewerkt, die n贸g kortere variant zou ook moeten werken.

Ik heb overigens in al je voorbeelden ook nog even het serienummer van je gasmeter weggehaald (47......34), de regel met 0-1:96.1.0. Er staan dus twee serienummers in DSMR v4 telegrammen.

Verkorte variant werkt ook!
En bedankt voor het aanpassen. Zal er op letten, hoef idd niet m'n serienummers de wereld in te gooien.

Voor de volledigheid nog even weer het complete script:

from time import sleep
import socket

import requests

HOST = '192.168.4.17'
PORT = 8088


API_SERVERS = (
    ('http://192.168.4.21/api/v1/datalogger/dsmrreading', '5S7SVP......O6ZJB'),
)


def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))

    while True:
        socket_data = s.recv(1024)
        telegram = str(socket_data, 'utf-8')

        print('Read telegram:')
        print(telegram)

        for current_server in API_SERVERS:
            api_url, api_key = current_server
            print('Sending telegram to:', api_url)
            send_telegram(telegram, api_url, api_key)

        sleep(1)


def send_telegram(telegram, api_url, api_key):
    response = requests.post(
        api_url,
        headers={'X-AUTHKEY': api_key},
        data={'telegram': telegram},
    )

    if response.status_code not in (200, 201):
        print('[!] API error: {}'.format(response.text))


if __name__ == '__main__':
    main()

Maar wat ik in m'n vorige comment zei, hoe zit het met foutafhandeling? De kans is natuurlijk aanwezig dat de ESP een keer vastloopt/netwerk een hikje geeft. Moet daar nog iets voor ingebouwd?

Top, bedankt voor je geduld en tests.

Ik sluit deze, mocht je toch nog nazorg hebben hierover dan hoor ik het hier wel.

De P1-kabel kan ik afkoppelen, mooi :) Maar hoe integreer ik dit in dsmr-reader ?
Kan ik in supervisor het dsmr_datalogger component uitschakelen? Dat zorgt voor het uitlezen van de ttyUSB0 toch? Of moet dat ook blijven draaien als ik het via de API erin duw?

  • Je kunt dsmr_datalogger uitcommenten met # 's in de Supervisor config. Je hebt die niet meer nodig als je de bovenstaande opzet gebruikt via de API.
  • Je kunt dan een nieuwe 'app' in Supervisor toevoegen met het voorbeeld onderaan deze pagina.

Ter referentie. Ik heb een Pi2 aan de meter hangen met kabel. De datalogger is daar uitgeschakeld en vervangen door de variant in bovenstaande docs link. In Supervisor ziet dat er zo uit:

dsmr_backend                     RUNNING
dsmr_client_datalogger           RUNNING
dsmr_webinterface                RUNNING

dsmr_client_datalogger (of hoe je hem ook wilt noemen) stuurt dus de metingen via de API door (ondanks dat de applicatie op hetzelfde apparaat staat). Daarnaast stuurt die ook de metingen door naar een andere, Pi3, die heel ergens anders staat. (als semi-productie-testomgeving).

Bij mij doet de dsmr_client_datalogger dus de uitlezingen via de P1-kabel, bij jou zou dat via die ESP gaan (beiden slechts transportmiddel).

Ik heb de dsmr_datalogger uitgeschakeld in supervisor en een eigen supervisor config gemaakt voor het Python-script wat je hebt gemaakt om de ESP8266 uit te lezen. Voor iemand die dit in de toekomst leest en ook ge茂nteresseerd is om de P1-poort van de slimme meter draadloos uit te lezen, ik heb het hele spulletje (incl. firmware voor ESP8266) hier online gezet.

Dennis, bedankt voor je hulp om dit werkend te krijgen!

Ik was hier toevallig net naar op zoek; eens kijken of het ook bij mij lukt :-D

Als ik handmatig het script van ThinkPadNL zijn site draai, krijg ik na enkele succesvolle telegrams:

Sending telegram to: http://-ip-/api/v1/datalogger/dsmrreading
[!] API error: Invalid data
Read telegram:

en dit blijft dan door'loop'en in de invalid data...

Op de 'automaat' via supervisor stopt hij ook met het verzenden van data na een paar succesvolle telegrams.

@Calimerorulez welke versie van DSMR heeft je meter?

Zie je dit als een van de eerste regels:

1-3:0.2.8(50) (v5.0)

of dit:

1-3:0.2.8(42) (v4.2)

Hey Dennis,

Dat is 1-3:0.2.8(42)

Ik dacht eerst dat het aan de combinatie met domoticz lag, omdat domoticz de netwerkpoort van de esp ook uitleest, maar als ik domoticz stop, wijzigt er niks aan het foutgaan.

Er komt een nieuwe patch de komende dagen, waarin beter wordt gelogd wat alle telegram-input is en welke falen/goed zijn (#224). Ik denk dat het even daarop wachten is en dat debuggen makkelijker maakt.

Ok, ik wacht af. Dank voor het mooie werk.

Ik merk overigens dat als ik een putty sessie open naar mijn esp, dat putty vanzelf afsluit na enkele geldige telegrammen, dus ik denk dat er in de esp-code/een telegram een raar (of een escape) character zit of iets dergelijks... Ik zal eens proberen te debuggen.

edit:
een 'nc' sessie naar mijn esp stopt er ook na enkele telegrammen mee; het lijkt erop dat de verbinding door de esp gesloten wordt o.i.d.

Ik gebruik domoticz ook. Als domoticz draait, dan stoort de boel elkaar lijkt het wel. Start ik domoticz, dan maakt domoticz een connectie en pollt goed. Zodra ik het script van ThinkPadNL ernaast start, dan logt domoticz een error, over een verloren connectie. Het script van ThankPadNL pollt wel goed, totdat domoticz een nieuwe poging waagt, na 30 seconden, om te connecten. Dan geeft dsmr-reader een 400 error (print(response.status_code)), ipv de correcte 200 status.

Stop ik domoticz helemaal, dan gaat het script van ThinkPadNL een tijdje langer goed, maar ook daarna komen er weer http 400-errors :-)

Ik heb nog even gekeken. Die Invalid data melding komt wanneer het POST-request op de API geen 'telegram' veld bevat.

Ik heb nu #224 klaar en daar zit ook betere logging in voor de API. Zodat je straks, als het goed is, kunt zien wat er binnenkomt.

Dit draait nu een dagje bij mij in productie en wanneer dat goed gaat komt de patch uit (waarschijnlijk morgen dus), samen met wat andere kleine dingetjes.

@Calimerorulez zojuist is de v1.5.2 patch uit met verbeterde logging. Aan de hand voor je input heb ik ook betere logging toegevoegd aan de API.

Wil je updaten naar de laatste versie?

Als dat is gelukt staan er als het goed is logfiles hier:

sudo su - dsmr

tail -f logs/dsmrreader.log 

(CTRL + C om het tailen af te breken)

Wil je aangeven of die nu een betere foutmelding geven? De 'Invalid data` zou in de logs nu zoiets moeten tonen:

API validation failed with POST data: X

Waarbij X de data is die meegestuurd wordt (dat zou een compleet telegram moeten zijn).

Let erop dat je hier (of uberhaupt op internet) niet je serienummers van je (2?) meters plaatst wanneer je een voorbeeld plakt. Dat zijn lange cijferreeksen van zo rond de 30 tekens aaneensluitend.

Ook de base64 data graag hier niet plaatsen. Mocht dat nodig zijn dan mag dat per e-mail.

Het uitlezen van de ESP8266 gaat hier nog steeds uitstekend:
p1esp8266 RUNNING pid 14642, uptime 4 days, 0:22:53
Nog steeds erg blij mee dat het nu werkt, geen lelijk USB-serial kabel meer in het zicht naar m'n server toe.

Als bij @Calimerorulez de boel er dus uit klapt dan ligt het waarschijnlijk aan de code op je ESP/de ESP zelf. Welke code gebruik je? Die van mijn Bitbucket ?

@dennissiemensma
in de log vind ik: [2017-01-10 18:00:35,765] WARNING @ views | API validation failed with POST data:

iedere seconde. Draai ik het schript van ThinkPadNL, dan zie ik 3 base64 encoded messages komen (tussendoor bovenstaande fout) en daarna weer continu om de seconde de bovenstaande fout.

@ThinkPadNL
Ik gebruik de code van Romix, vanaf https://github.com/ESP8266nu/ESPEasyPluginPlayground. Ik heb espeasy ingesteld zoals vermeld in de documentatie van Romix.

Als ik de komende dagen meer tijd heb, dan zal ik mijn hele esp upgraden. Ik draai nu op build 138.

Ik zal jouw code eens flashen. Ik snap alleen het nut niet van de specificatie van het te gebruiken protocol op de Config pagina van espeasy. Daar heb ik nu Domoticz HTTP staan.

edit: _P110_P1WifiGateway.ino van jouw bitbucket is aan de code te zien een html-pagina.. ;-)

@Calimerorulez Je zegt 'Iedere seconde draai ik het script van ThinkPad'. Maar hoe bedoel je dat precies? Je start het script en die blijft luisteren naar de ESP8266. E茅n keer starten is dus genoeg.

Ik heb de firmware van http://www.esp8266thingies.nl/wp/software/ gepakt, R124b. Die werkt dus, zoals hierboven in de output van supervisor te zien dus prima.

Bedankt voor de tip m.b.t. het .ino bestand. Ik heb het .ino bestand er nu op gezet ipv HTML-code.

Dank je voor je reactie. De tekst "iedere seconde", dat hoorde bij de regel ervoor. Ik start jouw script gewoon eenmalig op, en laat dat draaien.

Ik heb net de code van espeasy gepulled, P110 erin gehangen, want P1 smart meter zit er standaard niet in als device, dus heb ik de firmware via Arduino IDE zelf gecompileerd en geupload.

Ik heb een nieuwe Wemos D1 R2 genomen, geflasht, maar geen verschil. Als ik putty 'raw' connect op de esp poort, dan vliegt ie er ook na 3 telegrams uit. Ik zal R124b proberen.
Draai jij ook Domoticz?

Nee, geen Domoticz. Ik lees de P1 alleen uit naar dsmr-reader. Ik gebruik Home Assistant voor m'n domotica, maar doe daarin niks met de P1-poort.

Stappenplan:

  • Download deze firmware zodat je de tools hebt om de ESP te flashen zonder de Arduino IDE
  • Download R124b (hier zit de P1 al wel in de firmware)
  • Pak de ESPEasy_R120.zip uit.
  • Zet de ESPEasy.ino.d1_mini.R124b.bin in dezelfde map als flash.cmd en rename dat .bin bestand naar ESPEasy_R12*4*_4096.bin**
  • Dubbelklik op flash.cmd en voer de instellingen in (juiste COM-poort, flashsize 4096, build 124).

Als je dat allemaal hebt gedaan zou je die R124b firmware op de Wemos D1 moeten hebben. In de webinterface kun je dan de baudrate instellen en de poort (8088 bijv., die staat in mijn script ook ingevuld).

Die hele combinatie werkt dus erg betrouwbaar en zonder nukken bij mij. Met bovenstaande stappen hoef je dus ook niks te compilen of extra plugins toe te voegen. Die R124b.bin is voldoende aangezien daar alles al in zit.

Dank je, het werkt nu een stuk stabieler denk ik, hij blijft doorlopen. Ik heb de esp maar uit Domoticz verwijderd, want die blijft de connectie van jouw script om zeep helpen. Zodra Domoticz na 30 seconden opnieuw probeert te connecten naar de ESP (want Domoticz begint te klagen over een verloren connectie, als ik jouw script opstart), dan krijg ik weer de betreffende fout. Het lijkt erop dat twee processen/connecties tegelijk naar de esp de boel ophangt.

Ik experimenteer weer verder :-)

Dank!

Goedzo.
Waarom heb je Domoticz nog nodig als je dsmr-reader hebt draaien? Die kan hetzelfde en meer toch? ;)

Koudwatervrees ;-)

Wellicht dan een splitter ertussen zetten, dan kun je beide gebruiken ;-)

Wat betreft je foutmelding:
API validation failed with POST data: <QueryDict: {'telegram': ['']}>

Dit betekent dat er geen data binnenkomt, de telegram string is leeg. Dus de API call wordt gedaan, maar de data mist.

Hoe draait het nu @Calimerorulez ? Bij mij nog steeds als een trein, goede combi op deze manier. Ga binnenkort verhuizen en ben erg blij dat ik het op deze manier werkend heb kunnen krijgn.

Een ESP8266 in de meterkast is genoeg. Voeding kan dan via USB-poort van m'n router en m'n server waar VM met dsmr-reader kan bijv. op zolder komen te staan.

Hier ook als een trein, sinds ik hem uit domoticz gemikt heb 5 dagen geleden. Nogmaals dank voor de code en hulp :-)

Vandaag was de boel even stuk. Ik opende de webinterface van dsmr-reader toevallig even en zag dat er al 2 uur geen data meer was binnengekomen ( #208 is best wel gewenst 馃槈 ). De ESP herstart maar toen leek er nog steeds geen data binnen te komen. In supervisor het uitleesscript herstart en toen leek er weer data binnen te komen. Kan in de logging niet echt een duidelijke oorzaak vinden. Als #208 is ge茂mplementeerd dan kan ik ook sneller de logging controleren mocht het uitlezen weer stagneren.

Nog wel een verzoek, ik heb de logging in /var/log/supervisor bekeken en zag daar veel entries staan:

Sending telegram to: http://192.168.4.21/api/v1/datalogger/dsmrreading
[!] API error: Invalid data
Read telegram:

Wat ik eigenlijk mis is een datum en timestamp in de supervisor logging. Is dat eenvoudig toe te voegen in het ESP8266 uitleesscript wat ik gebruik?

Bovenin het script:

from datetime import datetime

Op de plek waar je de logging wil hebben:

print(datetime.now())

Damn you're fast! Heb het toegevoegd.

Gisteravond even bezig geweest met m'n netwerk en daardoor heeft de dsmr-reader VM geen contact gehad met de ESP8266 (of andersom). Het supervisor proces draaide nog wel gewoon. Maar omdat #208 nog niet geimplementeerd is heeft er dus 21 uur lang geen uitlezing plaatsgevonden....

Laatste wat er in de logfile stond is:

2017-01-23 19:46:57.486457
Sending telegram to: http://192.168.4.21/api/v1/datalogger/dsmrreading
2017-01-24 17:33:59.840615
Read telegram:
/KFM5KAIFA-METER

.... hier het hele telegram .... 
!65CA

Niet echt een foutmelding ofzo, daarna gewoon stilte dus.

Is er misschien iets in het Python-script toe te voegen dat hij een retry doet o.i.d.?

Ik zou verwachten dat die er helemaal uitklapt. Je zou nog kunnen proberen te forceren dat hij elke run de verbinding opent en weer sluit. En daardoor dus indirect het hele netwerk-gedeelte opnieuw doet.

Het socket gedeelte is verplaatst naar read_telegram():

def main():
    while True:
        telegram = read_telegram()
        print('Read telegram:')
        print(telegram)

        for current_server in API_SERVERS:
            api_url, api_key = current_server
            print('Sending telegram to:', api_url)
            send_telegram(telegram, api_url, api_key)

        sleep(1)


def read_telegram():
    telegram_start_seen = False
    telegram = ''

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))

    while True:
        print('Listening at socket...')
        socket_data = s.recv(1024)

        for data in socket_data.split(b"\n"):
            print("DATA LINE:", data)

            try:
                data = str(data, 'utf-8')
            except TypeError:
                pass

            if data.startswith('/'):
                telegram_start_seen = True

            if telegram_start_seen:
                telegram += data + "\n"

            if data.startswith('!') and telegram_start_seen:
                s.close()
                return telegram


def send_telegram(telegram, api_url, api_key):
    response = requests.post(
        api_url,
        headers={'X-AUTHKEY': api_key},
        data={'telegram': telegram},
    )

    if response.status_code not in (200, 201):
        print('[!] API error: {}'.format(response.text))


if __name__ == '__main__':
    main()

Bedankt, ik ga er even naar kijken. Hij klapt er inderdaad niet uit, waardoor supervisor ook niet hoeft in te grijpen. Daardoor lijkt alles in orde, maar komt er dus zomaar urenlang geen nieuwe data meer binnen.

Ik heb vanavond de wijzigingen uitgevoerd (zie bijgewerkte script hier), maar helaas werkt het nog niet. Na een netwerkonderbreking moet ik nog steeds via supervisor het uitleesscript herstarten voordat er weer nieuwe data binnenkomt.

Nog andere idee毛n?

Geen mooie oplossing, want ik heb hier helaas niet dezelfde setup om goed mee te testen. En al helemaal niet wanneer het script niet crasht met een exception maar 'hangt'.

Echter, een lelijke oplossing zou een uurlijkse cronjob kunnen zijn, die je datalogger herstart (in Supervisor). Je weet dan zeker dat de verbinding weer 'vers' is.

Die cron moet dan maar, beter dan niks. Krijg het alleen niet werkend...
0 * * * * /usr/bin/supervisorctl restart p1esp8266 >/dev/null 2>&1

Ook met root ervoor werkt het niet:
0 * * * * root /usr/bin/supervisorctl restart p1esp8266 >/dev/null 2>&1

Met de hand uitvoeren (moet wel sudo voor dan) laat het supervisor proces wel keurig herstarten...

Wat zegt de syslog in /var/log/syslog? Meestal staat daar nog een hint in, ook als het command goed uitgevoerd is.

Je kunt ook nog proberen om een script in /etc/cron.hourly/ te zetten met qua inhoud iets als: /usr/bin/supervisorctl restart p1esp8266

Dat script moet dan wel een chown root.root en een chmod 755 krijgen. Vervolgens kun je makkelijk testen met: run-parts -v /etc/cron.hourly (die voert alle scripts in die map uit)

Ook hier zouden eventuele fouten zichtbaar moeten zijn in ofwel het runparts-command of de syslog.

syslog zegt:

Feb  2 07:15:01 dsmrreader-vm CRON[24601]: (dsmr) CMD (root /usr/bin/supervisorctl restart p1esp8266 > /home/dsmr/dsmr-reader/cronlog.txt)
Feb  2 07:15:01 dsmrreader-vm CRON[24600]: (CRON) info (No MTA installed, discarding output)
Feb  2 07:15:01 dsmrreader-vm CRON[24602]: (root) CMD (command -v debian-sa1 > /dev/null && debian-sa1 1 1)

(/home/dsmr/dsmr-reader/cronlog.txt blijft leeg overigens ....)

Zal nog even stoeien met die cron.hourly

Het lijkt er wel op dat hij hem uitvoert, alleen zie ik in je bovenste regel (dsmr) staan. Kan het zijn dat de cron hem uitvoert als dsmr user? Je kunt meestal alleen een supervisorctl restart doen als root.

Heb je hem in /etc/crontab staan of ergens anders?

Daar zat hem het probleem inderdaad in, dat ik hem in de crontab van de dsmr user had gezet.
Switchen naar root (sudo su -) en dan crontab -e. Daar deze entry in gezet:
*/15 * * * * /usr/bin/supervisorctl restart p1esp8266 en nu werkt het herstarten van de supervisor taak.

Niet echt ideaal, maarja het moet maar. Misschien dat ik in de toekomst nog eens wat met Node-RED ga doen, dan kan ik wellicht de socket uitlezen (met retries e.d.) en de data in de API van dsmr-reader stoppen.

Hi all,

Sinds de update naar 1.13 krijg ik weer deze fout:

Sending telegram to: http:///api/v1/datalogger/dsmrreading
[!] API error: Invalid data

Nou, daar ben ik weer ;-)

Ik heb de firmware van mijn ESP-Easy geupgrade naar de laatste versie, en nu krijg ik (stom van me) API errors:

dsmrreader.log:

[2018-10-03 21:10:16,335] WARNING  @ v1 | API validation failed with POST data: <QueryDict: {'telegram': ['']}>
[2018-10-03 21:10:17,297] WARNING  @ services | Rejected telegram (base64 encoded): xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
[2018-10-03 21:10:17,299] ERROR    @ services | Content or CRC data not found
Traceback (most recent call last):
  File "/home/dsmr/dsmr-reader/dsmr_datalogger/services.py", line 189, in telegram_to_reading
    verify_telegram_checksum(data=data)
  File "/home/dsmr/dsmr-reader/dsmr_datalogger/services.py", line 123, in verify_telegram_checksum
    raise InvalidTelegramError('Content or CRC data not found')
dsmr_datalogger.exceptions.InvalidTelegramError: Content or CRC data not found

Mijn Python programma gooit dit eruit:

Read telegram:
/XMX5LGBBFG1009119848

1-3:0.2.8(42)
0-0:1.0.0(181003211857S)
0-0:96.1.1(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
1-0:1.8.1(004071.386*kWh)
1-0:1.8.2(004064.277*kWh)
1-0:2.8.1(000000.000*kWh)
1-0:2.8.2(000000.000*kWh)
0-0:96.14.0(0002)
1-0:1.7.0(00.580*kW)
1-0:2.7.0(00.000*kW)
0-0:96.7.21(00008)
0-0:96.7.9(00001)
1-0:99.97.0(1)(0-0:96.7.19)(160803065734S)(0000007136*s)
1-0:32.32.0(00000)
1-0:52.32.0(00000)
1-0:72.32.0(00001)
1-0:32.36.0(00000)
1-0:52.36.0(00000)
1-0:72.36.0(00000)
0-0:96.13.1()
0-0:96.13.0()
1-0:
Sending telegram to: http://192.168.188.5:8081/api/v1/datalogger/dsmrreading
[!] API error: Failed to parse telegram
Read telegram:
31.7.0(000*A)
1-0:51.7.0(000*A)
1-0:71.7.0(003*A)
1-0:21.7.0(00.008*kW)
1-0:41.7.0(00.003*kW)
1-0:61.7.0(00.568*kW)
1-0:22.7.0(00.000*kW)
1-0:42.7.0(00.000*kW)
1-0:62.7.0(00.000*kW)
0-1:24.1.0(003)
0-1:96.1.0(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
0-1:24.2.1(181003210000S)(01731.273*m3)
!5598

Het python script ziet er zo uit:

def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))

    while True:
        socket_data = s.recv(1024)
        telegram = str(socket_data, 'utf-8')

        print('Read telegram:')

        for current_server in API_SERVERS:
            api_url, api_key = current_server
            print(telegram)
            print('Sending telegram to:', api_url)
            send_telegram(telegram, api_url, api_key)

        sleep(1)


def send_telegram(telegram, api_url, api_key):
    response = requests.post(
        api_url,
        headers={'X-AUTHKEY': api_key},
        data={'telegram': telegram},
    )

    if response.status_code not in (200, 201):
        print('[!] API error: {}'.format(response.text))


if __name__ == '__main__':
    main()

Iemand een idee? :-)

Het enige wat ik je kan adviseren is terug gaan naar de versie die wel werkt. Ik heb wel gemerkt dat je liever niet aan de ESP moet zitten als het gewoon werkt :)

Was this page helpful?
0 / 5 - 0 ratings