Logo TheStaticTurtle


LinkyLink - Connecting myself to the French energy meter

Making a custom board to connect an esp8266 to the french energy meter



A bit of context

The Linky energy meter is "connected" energy meter it transmit the energy consumed by their user to the energy company (Enedis).

The inner workings

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:

Version 1 - Prototype

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:

A wireless adapter for the french energy meter (Linky) based on an esp8266. - TheStaticTurtle/LinkyLink
GitHub - TheStaticTurtle/LinkyLink: A wireless adapter for the french energy meter (Linky) based on an esp8266.

A wireless adapter for the french energy meter (Linky) based on an esp8266. - TheStaticTurtle/LinkyLink

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.

CommentsShortcut to: Comments

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