Compare commits

...

10 Commits

Author SHA1 Message Date
Gitouche 419d29f5be adapté 2023-01-02 13:17:07 +01:00
Sébastien Reuiller 2dc277e8a4 pep8 et constante en majuscule 2021-12-14 23:55:27 +01:00
Sébastien Reuiller 0702cc0801 utilisation de la fonction checksum et simplification 2021-12-14 23:40:57 +01:00
SebastienReuiller 9df0308204
Merge pull request #7 from Charlymd/checksum
ajout de la fonction de calcul du checksum
2021-12-14 22:52:11 +01:00
Charlymd fbf2010dca correction bug checksum sur caractere 0x32 (espace) 2020-08-29 16:39:04 +02:00
Charlymd ebd786e3a3 checksum en UTF-8 erreur sur caractere 0x32 2020-08-28 22:43:23 +02:00
Charlymd 01e61f5eff ajout de la fonction de calcul du checksum 2020-08-26 09:06:42 +02:00
Sébastien Reuiller a4a6e2fd38 modification URL de la capture 2019-11-19 10:14:52 +01:00
Sébastien Reuiller 238617e547 tableau de bord avec tarif 2019-06-19 00:01:17 +02:00
Sébastien Reuiller 58ce8152c6 ajout image + lien vers blog 2019-02-13 23:42:59 +01:00
5 changed files with 388 additions and 244 deletions

3
.pylintrc Normal file
View File

@ -0,0 +1,3 @@
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=119

View File

@ -1,2 +1,11 @@
# Téléinfo Linky avec un Raspberry Pi
Surveiller sa consommation électrique en temps réel avec un compteur Linky et un Raspberry
![Monitorer son compteur Linky avec Grafana](https://sebastienreuiller.fr/blog/wp-content/uploads/2019/10/capture_teleinfo_avec_tarif.png)
## Documentation
Tuto pas à pas sur mon blog : [Monitorer son compteur Linky avec Grafana, cest possible.. et ça tourne sur un Raspberry Pi!](https://sebastienreuiller.fr/blog/monitorer-son-compteur-linky-avec-grafana-cest-possible-et-ca-tourne-sur-un-raspberry-pi/)

View File

@ -1,8 +1,8 @@
{
"__inputs": [
{
"name": "DS_INFLUXLOCAL",
"label": "InfluxLocal",
"name": "DS_TELEINFO",
"label": "Teleinfo",
"description": "",
"type": "datasource",
"pluginId": "influxdb",
@ -63,7 +63,7 @@
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": "${DS_INFLUXLOCAL}",
"datasource": "${DS_TELEINFO}",
"decimals": 6,
"format": "watth",
"gauge": {
@ -75,7 +75,7 @@
},
"gridPos": {
"h": 3,
"w": 10,
"w": 5,
"x": 0,
"y": 0
},
@ -152,6 +152,124 @@
],
"valueName": "avg"
},
{
"cacheTimeout": null,
"colorBackground": true,
"colorValue": false,
"colors": [
"#299c46",
"rgb(185, 141, 18)",
"#d44a3a"
],
"datasource": "${DS_TELEINFO}",
"format": "currencyEUR",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 3,
"w": 5,
"x": 5,
"y": 0
},
"id": 15,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "",
"targets": [
{
"alias": "HP",
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"measurement": "HCHC",
"orderByTime": "ASC",
"policy": "default",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
},
{
"params": [
"*0.00007380"
],
"type": "math"
}
]
],
"tags": []
}
],
"thresholds": "",
"title": "Tarif HC Période",
"transparent": false,
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "diff"
},
{
"cacheTimeout": null,
"colorBackground": false,
@ -161,7 +279,7 @@
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": "${DS_INFLUXLOCAL}",
"datasource": "${DS_TELEINFO}",
"format": "watth",
"gauge": {
"maxValue": 100,
@ -237,7 +355,7 @@
}
],
"thresholds": "",
"title": "Tarif",
"title": "Tarif en cours",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
@ -249,6 +367,124 @@
],
"valueName": "avg"
},
{
"cacheTimeout": null,
"colorBackground": true,
"colorValue": false,
"colors": [
"#299c46",
"#508642",
"#d44a3a"
],
"datasource": "${DS_TELEINFO}",
"format": "currencyEUR",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 3,
"w": 5,
"x": 14,
"y": 0
},
"id": 14,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "",
"targets": [
{
"alias": "HP",
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"measurement": "HCHP",
"orderByTime": "ASC",
"policy": "default",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
},
{
"params": [
"*0.00009790"
],
"type": "math"
}
]
],
"tags": []
}
],
"thresholds": "",
"title": "Tarif HP Période",
"transparent": false,
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "diff"
},
{
"cacheTimeout": null,
"colorBackground": false,
@ -258,7 +494,7 @@
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": "${DS_INFLUXLOCAL}",
"datasource": "${DS_TELEINFO}",
"decimals": 6,
"format": "watth",
"gauge": {
@ -270,8 +506,8 @@
},
"gridPos": {
"h": 3,
"w": 10,
"x": 14,
"w": 5,
"x": 19,
"y": 0
},
"id": 11,
@ -352,7 +588,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_INFLUXLOCAL}",
"datasource": "${DS_TELEINFO}",
"fill": 1,
"gridPos": {
"h": 9,
@ -482,7 +718,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_INFLUXLOCAL}",
"datasource": "${DS_TELEINFO}",
"fill": 1,
"gridPos": {
"h": 9,
@ -643,7 +879,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_INFLUXLOCAL}",
"datasource": "${DS_TELEINFO}",
"fill": 1,
"gridPos": {
"h": 9,
@ -745,7 +981,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_INFLUXLOCAL}",
"datasource": "${DS_TELEINFO}",
"fill": 1,
"gridPos": {
"h": 9,
@ -842,170 +1078,6 @@
"align": true,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_INFLUXLOCAL}",
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 21
},
"hideTimeOverride": false,
"id": 7,
"interval": "10s",
"legend": {
"alignAsTable": true,
"avg": false,
"current": true,
"hideEmpty": false,
"hideZero": false,
"max": true,
"min": true,
"rightSide": false,
"show": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": true,
"steppedLine": false,
"targets": [
{
"alias": "hp",
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"linear"
],
"type": "fill"
}
],
"hide": false,
"measurement": "HCHP",
"orderByTime": "ASC",
"policy": "default",
"query": "SELECT mean(\"value\")::integer AS value FROM \"HCHC\" WHERE $timeFilter GROUP BY time(1s) fill(linear)",
"rawQuery": false,
"refId": "B",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mode"
}
]
],
"tags": []
},
{
"alias": "hc",
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"linear"
],
"type": "fill"
}
],
"hide": false,
"measurement": "HCHC",
"orderByTime": "ASC",
"policy": "default",
"query": "SELECT mean(\"value\")::integer AS value FROM \"HCHC\" WHERE $timeFilter GROUP BY time(1s) fill(linear)",
"rawQuery": false,
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mode"
}
]
],
"tags": []
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "HC + HP",
"tooltip": {
"shared": false,
"sort": 1,
"value_type": "individual"
},
"transparent": false,
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "watth",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}
],
"yaxis": {
"align": true,
"alignLevel": null
}
}
],
"refresh": "10s",
@ -1047,5 +1119,5 @@
"timezone": "",
"title": "Conso Elec",
"uid": "vrq6Vgigz",
"version": 36
}
"version": 37
}

24
setup.cfg Normal file
View File

@ -0,0 +1,24 @@
# - https://timothycrosley.github.io/isort/
# - https://github.com/timothycrosley/isort#configuring-isort
# - https://github.com/timothycrosley/isort/wiki/isort-Settings
[isort]
combine_as_imports = True
ensure_newline_before_comments = True
force_grid_wrap = 0
include_trailing_comma = True
known_first_party = itou
lines_after_imports = 2
line_length = 119
multi_line_output = 3
use_parentheses = True
# - https://www.flake8rules.com
# - http://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html#changing-the-ignore-list
[flake8]
# E203: Whitespace before ':', used to please Black in `yield items[i : i + n]`
# E266: Too many leading '#' for block comment
# W503: Line break occurred before a binary operator
ignore = E203, E266, W503
max-line-length = 119

166
teleinfo.py Normal file → Executable file
View File

@ -3,51 +3,69 @@
# __author__ = "Sébastien Reuiller"
# __licence__ = "Apache License 2.0"
# Python 3, prerequis : pip install pySerial influxdb
#
# Exemple de trame:
# {
# 'OPTARIF': 'HC..', # option tarifaire
# 'IMAX': '007', # intensité max
# 'HCHC': '040177099', # index heure creuse en Wh
# 'IINST': '005', # Intensité instantanée en A
# 'PAPP': '01289', # puissance Apparente, en VA
# 'MOTDETAT': '000000', # Mot d'état du compteur
# 'HHPHC': 'A', # Horaire Heures Pleines Heures Creuses
# 'ISOUSC': '45', # Intensité souscrite en A
# 'ADCO': '000000000000', # Adresse du compteur
# 'HCHP': '035972694', # index heure pleine en Wh
# 'PTEC': 'HP..' # Période tarifaire en cours
# }
# Python 3, prérequis : pip install pySerial influxdb
import serial
import logging
import time
import requests
from datetime import datetime
import requests
import serial
from influxdb import InfluxDBClient
# Nombre de secondes entre deux transmission des mesures
DELAY_MESURE = 10
# clés à utiliser - les autres ne seront pas transmises
USED_MESURE_KEYS = [
'LTARF',
'EAST',
'EASF01',
'EASF02',
'IRMS1',
'URMS1',
'SINSTS',
'SMAXSN',
'CCASN',
'UMOY1',
'MSG1',
'NTARF',
]
# clés téléinfo
int_measure_keys = ['IMAX', 'HCHC', 'IINST', 'PAPP', 'ISOUSC', 'ADCO', 'HCHP']
INT_MESURE_KEYS = [
'EAST',
'EASF',
'EASD',
'EAIT',
'ERQ',
'IRMS',
'URMS',
'PREF',
'PCOUP',
'SINST',
'SMAX',
'CCA',
'UMOY',
]
# création du logguer
logging.basicConfig(filename='/var/log/teleinfo/releve.log', level=logging.INFO, format='%(asctime)s %(message)s')
logging.basicConfig(filename='/tmp/teleinfo-releve.log', level=logging.INFO, format='%(asctime)s %(message)s')
logging.info("Teleinfo starting..")
# connexion a la base de données InfluxDB
client = InfluxDBClient('localhost', 8086)
db = "teleinfo"
client = InfluxDBClient('192.168.0.10', 8086)
DB_NAME = "teleinfo"
connected = False
while not connected:
try:
logging.info("Database %s exists?" % db)
if not {'name': db} in client.get_list_database():
logging.info("Database %s creation.." % db)
client.create_database(db)
logging.info("Database %s created!" % db)
client.switch_database(db)
logging.info("Connected to %s!" % db)
logging.info("Database %s exists?" % DB_NAME)
if not {'name': DB_NAME} in client.get_list_database():
logging.info("Database %s creation.." % DB_NAME)
client.create_database(DB_NAME)
logging.info("Database %s created!" % DB_NAME)
client.switch_database(DB_NAME)
logging.info("Connected to %s!" % DB_NAME)
except requests.exceptions.ConnectionError:
logging.info('InfluxDB is not reachable. Waiting 5 seconds to retry.')
time.sleep(5)
@ -55,31 +73,37 @@ while not connected:
connected = True
def add_measures(measures, time_measure):
def add_measures(measures):
points = []
for measure, value in measures.items():
for measure, values in measures.items():
point = {
"measurement": measure,
"tags": {
"host": "raspberry",
"region": "linky"
},
"time": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
"fields": {
"value": value
}
}
"measurement": measure,
"tags": {
# identification de la sonde et du compteur
"host": "raspberry",
"region": "linky"
},
"time": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
"fields": values
}
points.append(point)
client.write_points(points)
def verif_checksum(data, checksum):
data_unicode = 0
for caractere in data:
data_unicode += ord(caractere)
sum_unicode = (data_unicode & 63) + 32
return (checksum == chr(sum_unicode))
def checksum(line:str) -> str:
return chr((sum(list(line)) & 0x3F) + 0x20)
def main():
with serial.Serial(port='/dev/ttyS0', baudrate=1200, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE,
bytesize=serial.SEVENBITS, timeout=1) as ser:
logging.info("Teleinfo is reading on /dev/ttyS0..")
with serial.Serial(port='/dev/ttyAMA0', baudrate=9600, parity=serial.PARITY_EVEN, bytesize=serial.SEVENBITS, timeout=1) as ser:
logging.info("Teleinfo is reading on /dev/ttyAMA0..")
trame = dict()
# boucle pour partir sur un début de trame
@ -87,40 +111,52 @@ def main():
while b'\x02' not in line: # recherche du caractère de début de trame
line = ser.readline()
# lecture de la première ligne de la première trame
line = ser.readline()
delaycounter = DELAY_MESURE
while True:
line_str = line.decode("utf-8")
ar = line_str.split(" ")
while delaycounter < DELAY_MESURE:
ser.read_until(b'\x03')
delaycounter+=1
line = ser.readline()
logging.debug(line)
try:
key = ar[0]
if key in int_measure_keys :
value = int(ar[1])
dataset = line.replace(b'\x03\x02', b'').rstrip(b'\r\n')
checksumchar = checksum(dataset[:-1])
if checksumchar == chr(dataset[-1]):
spline = dataset.split(b'\t')
logging.debug(spline)
etiquette = spline[0].decode('ascii')
if etiquette in USED_MESURE_KEYS:
value = spline[1].decode('ascii')
timestamp = None
if len(spline) == 4 and spline[2] != b'' : # Horodaté
value = spline[2].decode('ascii')
timestamp = spline[1].decode('ascii')
for radix in INT_MESURE_KEYS:
if etiquette.startswith(radix):
value = int(value)
trame[etiquette] = {
"value": value,
"timestamp": timestamp,
}
else:
value = ar[1]
checksum = ar[2]
trame[key] = value
logging.debug('Checksum error, aborting frame')
if b'\x03' in line: # si caractère de fin dans la ligne, on insère la trame dans influx
del trame['ADCO'] # adresse du compteur : confidentiel!
time_measure = time.time()
# insertion dans influxdb
add_measures(trame, time_measure)
add_measures(trame)
# ajout timestamp pour debugger
trame["timestamp"] = int(time_measure)
logging.debug(trame)
trame = dict() # on repart sur une nouvelle trame
delaycounter=0
except Exception as e:
logging.error("Exception : %s" % e)
line = ser.readline()
logging.error("Exception : %s" % e, exc_info=True)
logging.error("%s" % (line))
if __name__ == '__main__':
if connected:
main()