Making a custom board to connect an esp8266 to the french energy meter
The Linky energy meter is "connected" energy meter it transmit the energy consumed by their user to the energy company (Enedis).
The arrow on the second picture above show the user teleinfo port. Here the user is free to plug something like the "Linky ERL" or any device to monitor the meter
The linky provides 3 "easy" to use ports I1 I2 and A. The actual data comes from the circuit I1 and I2 and you have an alimentation circuit between I1 and A. For reference P, N, P', N' are the mains connections C1 and C2 connection to a water boiler and T1 / T2 are I think for technicians and are locked.
The actual signal is a unidirectional serial connection with a 50kHz carrier. The documentation (available at the bottom) says that the signal is at 9600bps 7E1 but the signal is actually 1200bps at 7E1. Converting this signal is really simple I used the following circuit:
The linky spits out frames composed of these lines (For the one I have):
ADCO <censored>
OPTARIF HC..
ISOUSC 30
HCHC 000582078
HCHP 000599002
PTEC HP..
IINST 005
IMAX 090
PAPP 01115
HHPHC A
MOTDETAT 000000
Here's a table to see what stands for what:
The documentation tells us how a frame is transmitted: First you have the char 0x02 there for each line start 0x0A and 0x0D the end of a line the end of a frame is signaled by the char 0x03 and a premature end of data by 0x04
1#define LINKY_START_FRAME 0x02
2#define LINKY_END_FRAME 0x03
3#define LINKY_START_LINE 0x0A
4#define LINKY_END_LINE 0x0D
5#define LINKY_END_DATA 0x04
As I wanted to make my module compatible with 3-phased devices I added all these in the code as well and here's my structure for holding the data:
1struct LinkyData {
2 char ADCO [13]; //Adresse du compteur
3 char OPTARIF[ 5]; //Option tarifaire choisie
4 int ISOUSC; //Intensitée souscrite
5 int BASE; //Index option Base
6 long HCHC; //Index option Heures Creuses - Heures Creuses
7 long HCHP; //Index option Heures Creuses - Heures Pleines
8 long EJPHN; //Index option EJP - Heures Normales
9 long EJPHPM; //Index option EJP - Heures de Pointe Mobile
10 long BBRHCJB; //Index option Tempo - Heures Creuses Jours Bleus
11 long BBRHPJB; //Index option Tempo - Heures Pleines Jours Bleus
12 long BBRHCJW; //Index option Tempo - Heures Creuses Jours Blancs
13 long BBRHPJW; //Index option Tempo - Heures Pleines Jours Blancs
14 long BBRHCJR; //Index option Tempo - Heures Pleines Jours Rouges
15 long BBRHPJR; //Index option Tempo - Heures Pleines Jours Rouges
16 int PEJP; //Préavis Début EJP (30 min)
17 char PTEC [ 5]; //Période Tarifaire en cours
18 char DEMAIN [ 5]; //Couleur du lendemain
19 int IINST; //Intensité Instantanée
20 int IINST1; //Intensité Instantanée Phase né1 (Triphaser seulement)
21 int IINST2; //Intensité Instantanée Phase né2 (Triphaser seulement)
22 int IINST3; //Intensité Instantanée Phase né3 (Triphaser seulement)
23 int ADPS; //Avertissement de DépassementDe Puissance Souscrite
24 int ADIR1; //Avertissement de DépassementDe Puissance Souscrite Phase né1 (Triphaser seulement)
25 int ADIR2; //Avertissement de DépassementDe Puissance Souscrite Phase né2 (Triphaser seulement)
26 int ADIR3; //Avertissement de DépassementDe Puissance Souscrite Phase né3 (Triphaser seulement)
27 int IMAX; //Intensité maximale appelée
28 int IMAX1; //Intensité maximale appelée Phase né1 (Triphaser seulement)
29 int IMAX2; //Intensité maximale appelée Phase né2 (Triphaser seulement)
30 int IMAX3; //Intensité maximale appelée Phase né3 (Triphaser seulement)
31 long PMAX; //Puissance maximale triphasée atteinte
32 long PAPP; //Puissance apparente / Puissance apparente triphasée soutirée
33 char HHPHC; //Horaire Heures Pleines Heures Creuses
34 char MOTDETAT[ 7]; //Mot d'état du compteur
35 char PPOT [ 3]; //Présence des potentiels (Triphaser seulement) ("0X", X = coupures de phase phase n => bit n = 1)
36};
Receiving the data is as simple as creating a new software serial with the right pins and the incoming byte by 0x7F to get only the 7 bits since arduino software serial can't strictly do 7E1 communications. Then I created a char array of the size of 100 (It will never be used completely) and incremented a variable at each valid char received to store it.
1void Linky::processRXChar(char currentChar) {
2 if(currentChar == LINKY_START_FRAME) {
3 memset(_buffer, 0, LINKY_BUFFER_TELEINFO_SIZE);
4 _bufferIterator = 0;
5
6 } else if(currentChar == LINKY_START_LINE) {
7 memset(_buffer, 0, LINKY_BUFFER_TELEINFO_SIZE);
8 _bufferIterator = 0;
9
10 } else if(currentChar == LINKY_END_FRAME) {
11 memset(_buffer, 0, LINKY_BUFFER_TELEINFO_SIZE);
12 _bufferIterator = 0;
13
14 } else if(currentChar == LINKY_END_LINE) {
15 updateStruct(_bufferIterator);
16 memset(_buffer, 0, LINKY_BUFFER_TELEINFO_SIZE);
17 _bufferIterator = 0;
18
19 } else if(currentChar == LINKY_END_DATA) {
20 memset(_buffer, 0, LINKY_BUFFER_TELEINFO_SIZE);
21 _bufferIterator = 0;
22
23 } else {
24 _buffer[_bufferIterator] = currentChar;
25 _bufferIterator++;
26
27 }
28}
29void Linky::updateAsync() {
30 if(_serport->available()) {
31 char currentChar;
32
33 currentChar = _serport->read() & 0x7F;
34 processRXChar(currentChar);
35 }
36}
Then I created a couple of function that ease the parsing of the data by a lot (getCommandValue_int / getCommandValue_long / getCommandValue_str) to use these function you have to pass the actual command name like ADCO the length in this case 4 and the expected length of the output in this case 12 plus some other values like the size of received line.
1bool Linky::isValidNumber(String str){
2 for(byte i=0;i<str.length();i++) {
3 if(!isDigit(str.charAt(i))) return false;
4 }
5 return true;
6}
7
8int Linky::getCommandValue_int(String CMD, int CMDlenght, int CMDResultLenght, String line, int lineLenght, int defaultIfError) {
9 if(!line.startsWith(CMD)) { return defaultIfError; }
10 if(CMDlenght+1+CMDResultLenght > lineLenght) { return defaultIfError; } // Invalid size line length too short
11
12 String raw_value = line.substring(CMDlenght+1, CMDlenght+1+CMDResultLenght);
13 if(!isValidNumber(raw_value)) { return defaultIfError; }
14
15 return raw_value.toInt();
16}
17
18long Linky::getCommandValue_long(String CMD, int CMDlenght, int CMDResultLenght, String line, int lineLenght, long defaultIfError) {
19 if(!line.startsWith(CMD)) { return defaultIfError; }
20 if(CMDlenght+1+CMDResultLenght > lineLenght) { return defaultIfError; } // Invalid size line length too short
21
22 String raw_value = line.substring(CMDlenght+1, CMDlenght+1+CMDResultLenght);
23 if(!isValidNumber(raw_value)) { return defaultIfError; }
24
25 return (long)raw_value.toInt();
26}
27
28bool Linky::getCommandValue_str(String CMD, int CMDlenght, int CMDResultLenght, String line, int lineLenght, char* value) {
29 if(!line.startsWith(CMD)) { return false; }
30 if(CMDlenght+1+CMDResultLenght > lineLenght) { return false; } // Invalid size line length too short
31
32 line.substring(CMDlenght+1, CMDlenght+1+CMDResultLenght).toCharArray(value,CMDResultLenght+1);
33 return true;
34}
35
36void Linky::updateStruct(int len) {
37 getCommandValue_str ("ADCO" , 4,12,_buffer,len+1, _data.ADCO );
38 getCommandValue_str ("OPTARIF" , 7, 4,_buffer,len+1, _data.OPTARIF );
39 _data.ISOUSC = getCommandValue_int ("ISOUSC" , 6, 2,_buffer,len+1, _data.ISOUSC);
40 _data.HCHC = getCommandValue_long("HCHC" , 4, 9,_buffer,len+1, _data.HCHC);
41 _data.HCHP = getCommandValue_long("HCHP" , 4, 9,_buffer,len+1, _data.HCHP);
42 _data.EJPHN = getCommandValue_long("EJPHN" , 5, 9,_buffer,len+1, _data.EJPHN);
43 _data.EJPHPM = getCommandValue_long("EJPHPM" , 6, 9,_buffer,len+1, _data.EJPHPM);
44 _data.BBRHCJB = getCommandValue_long("BBRHCJB" , 7, 9,_buffer,len+1, _data.BBRHCJB);
45 _data.BBRHPJB = getCommandValue_long("BBRHPJB" , 7, 9,_buffer,len+1, _data.BBRHPJB);
46 _data.BBRHCJW = getCommandValue_long("BBRHCJW" , 7, 9,_buffer,len+1, _data.BBRHCJW);
47 _data.BBRHPJW = getCommandValue_long("BBRHPJW" , 7, 9,_buffer,len+1, _data.BBRHPJW);
48 _data.BBRHCJR = getCommandValue_long("BBRHCJR" , 7, 9,_buffer,len+1, _data.BBRHCJR);
49 _data.BBRHPJR = getCommandValue_long("BBRHPJR" , 7, 9,_buffer,len+1, _data.BBRHPJR);
50 _data.PEJP = getCommandValue_int ("PEJP" , 4, 2,_buffer,len+1, _data.HCHP);
51 getCommandValue_str ("PTEC" , 4, 4,_buffer,len+1, _data.PTEC );
52 getCommandValue_str ("DEMAIN" , 6, 4,_buffer,len+1, _data.DEMAIN );
53 _data.IINST = getCommandValue_int ("IINST" , 5, 3,_buffer,len+1, _data.IINST);
54 _data.IINST1 = getCommandValue_int ("IINST1" , 6, 3,_buffer,len+1, _data.IINST1);
55 _data.IINST2 = getCommandValue_int ("IINST2" , 6, 3,_buffer,len+1, _data.IINST2);
56 _data.IINST3 = getCommandValue_int ("IINST3" , 6, 3,_buffer,len+1, _data.IINST3);
57 _data.ADPS = getCommandValue_int ("ADPS" , 4, 3,_buffer,len+1, _data.ADPS);
58 _data.ADIR1 = getCommandValue_int ("ADIR1" , 5, 3,_buffer,len+1, _data.ADIR1);
59 _data.ADIR2 = getCommandValue_int ("ADIR2" , 5, 3,_buffer,len+1, _data.ADIR2);
60 _data.ADIR3 = getCommandValue_int ("ADIR3" , 5, 3,_buffer,len+1, _data.ADIR3);
61 _data.IMAX = getCommandValue_int ("IMAX" , 4, 3,_buffer,len+1, _data.IMAX);
62 _data.IMAX1 = getCommandValue_int ("IMAX1" , 5, 3,_buffer,len+1, _data.IMAX1);
63 _data.IMAX2 = getCommandValue_int ("IMAX2" , 5, 3,_buffer,len+1, _data.IMAX2);
64 _data.IMAX3 = getCommandValue_int ("IMAX3" , 5, 3,_buffer,len+1, _data.IMAX3);
65 _data.PMAX = getCommandValue_long("PMAX" , 4, 5,_buffer,len+1, _data.PMAX);
66 _data.PAPP = getCommandValue_long("PAPP" , 4, 5,_buffer,len+1, _data.PAPP);
67 getCommandValue_str ("MOTDETAT", 8, 6,_buffer,len+1, _data.MOTDETAT );
68 getCommandValue_str ("PPOT" , 4, 2,_buffer,len+1, _data.PPOT);
69}
Implementing everything into the ESP8266:
The code is working on some meters but on some other I cannot get data to be read by the esp8266 it seems to be an optocoupler issue.
Want to chat about this article? Just post a message down here. Chat is powered by giscus and all discussions can be found here: TheStaticTurtle/blog-comments