/* xnrg_15_Teleinfo.ino - Teleinfo support for Tasmota Copyright (C) 2021 Charles-Henri Hallard This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_ENERGY_SENSOR #ifdef USE_TELEINFO /*********************************************************************************************\ * Teleinfo : French energy provider metering telemety data * Source: http://hallard.me/category/tinfo/ * * Denky ESP32 Teleinfo Template * {"NAME":"Denky (Teleinfo)","GPIO":[1,1,1,1,5664,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,0,1376,1,1,0,0,0,0,1,5632,1,1,1,0,0,1],"FLAG":0,"BASE":1} * * Denky (aka WifInfo) ESP8266 Teleinfo Template * {"NAME":"WifInfo v1.0a","GPIO":[17,255,255,255,6,5,255,255,7,210,255,255,255],"FLAG":15,"BASE":18} * {"NAME":"WifInfo","GPIO":[7,255,255,210,6,5,255,255,255,255,255,255,255],"FLAG":15,"BASE":18} * \*********************************************************************************************/ #define XNRG_15 15 #include "LibTeleinfo.h" #include #define TINFO_READ_TIMEOUT 400 // All contract type for legacy, standard mode has in clear text enum TInfoContrat{ CONTRAT_BAS = 1, // BASE => Option Base. CONTRAT_HC, // HC.. => Option Heures Creuses. CONTRAT_EJP, // EJP. => Option EJP. CONTRAT_BBR, // BBRx => Option Tempo CONTRAT_END }; // contract displayed name for legacy, standard mode has in clear text const char kContratName[] PROGMEM = "|Base|Heures Creuses|EJP|Bleu Blanc Rouge" ; // Received current contract value for legacy, standard mode has in clear text const char kContratValue[] PROGMEM = "|BASE|HC..|EJP.|BBR" ; // all tariff type for legacy, standard mode has in clear text enum TInfoTarif{ TARIF_TH = 1, TARIF_HC, TARIF_HP, TARIF_HN, TARIF_PM, TARIF_CB, TARIF_CW, TARIF_CR, TARIF_PB, TARIF_PW, TARIF_PR, TARIF_END }; // Received current tariff values // for legacy, standard mode has in clear text const char kTarifValue[] PROGMEM = "|TH..|HC..|HP.." "|HN..|PM.." "|HCJB|HCJW|HCJR" "|HPJB|HPJW|HPJR" ; // tariff displayed name (for legacy, standard mode has in clear text) const char kTarifName[] PROGMEM = "|Toutes|Creuses|Pleines" "|Normales|Pointe Mobile" "|Creuses Bleu|Creuses Blanc|Creuse Rouges" "|Pleines Bleu|Pleines Blanc|Pleines Rouges" ; // Label used to do some post processing and/or calculation enum TInfoLabel{ LABEL_BASE = 1, LABEL_ADCO, LABEL_ADSC, LABEL_HCHC, LABEL_HCHP, LABEL_EAST, LABEL_EASF01, LABEL_EASF02, LABEL_OPTARIF, LABEL_NGTF, LABEL_ISOUSC, LABEL_PREF, LABEL_PTEC, LABEL_LTARF, LABEL_NTARF, LABEL_PAPP, LABEL_SINSTS, LABEL_IINST, LABEL_IRMS1, LABEL_TENSION, LABEL_URMS1, LABEL_IMAX, LABEL_PMAX, LABEL_SMAXSN, LABEL_DEMAIN, LABEL_END }; const char kLabel[] PROGMEM = "|BASE|ADCO|ADSC" "|HCHC|HCHP|EAST|EASF01|EASF02" "|OPTARIF|NGTF|ISOUSC|PREF|PTEC|LTARF|NTARF" "|PAPP|SINSTS|IINST|IRMS1|TENSION|URMS1" "|IMAX|PMAX|SMAXSN" "|DEMAIN" ; #define TELEINFO_SERIAL_BUFFER_STANDARD 512 // Receive buffer size for Standard mode #define TELEINFO_SERIAL_BUFFER_HISTORIQUE 512 // Receive buffer size for Legacy mode #define TELEINFO_PROCESS_BUFFER 32 // Local processing buffer TInfo tinfo; // Teleinfo object TasmotaSerial *TInfoSerial = nullptr; _Mode_e tinfo_mode = TINFO_MODE_HISTORIQUE; char serialNumber[13] = ""; // Serial number is 12 char long bool tinfo_found = false; int contrat; int tarif; int isousc; /*********************************************************************************************/ /* ====================================================================== Function: getValueFromLabelIndex Purpose : return label value from label index Input : label index to search for Output : value filled Comments: - ====================================================================== */ char * getValueFromLabelIndex(int labelIndex, char * value) { char labelName[16]; // Get the label name GetTextIndexed(labelName, sizeof(labelName), labelIndex, kLabel); // Get value of label name return tinfo.valueGet(labelName, value) ; } /* ====================================================================== Function: ADPSCallback Purpose : called by library when we detected a ADPS on any phased Input : phase number 0 for ADPS (monophase) 1 for ADIR1 triphase 2 for ADIR2 triphase 3 for ADIR3 triphase Output : - Comments: should have been initialised with a tinfo.attachADPSCallback(ADPSCallback()) ====================================================================== */ void ADPSCallback(uint8_t phase) { // n = phase number 1 to 3 if (phase == 0){ phase = 1; } Response_P(PSTR("{")); ResponseAppend_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("{\"%s\":{\"ADPS\":%i}}"), serialNumber, phase ); ResponseJsonEnd(); // Publish adding ADCO serial number into the topic MqttPublishPrefixTopic_P(RESULT_OR_TELE, serialNumber, false); AddLog(LOG_LEVEL_INFO, PSTR("ADPS on phase %d"), phase); } /* ====================================================================== Function: DataCallback Purpose : callback when we detected new or modified data received Input : linked list pointer on the concerned data current flags value Output : - Comments: - ====================================================================== */ void DataCallback(struct _ValueList * me, uint8_t flags) { char c = ' '; int ilabel ; // Does this value is new or changed? if (flags & (TINFO_FLAGS_ADDED | TINFO_FLAGS_UPDATED) ) { char labelName[16]; // Find the label index for ( ilabel = 1 ; ilabel < LABEL_END ; ilabel++) { GetTextIndexed(labelName, sizeof(labelName), ilabel, kLabel); if (!strcmp(labelName, me->name)) { break; } } if (flags & TINFO_FLAGS_ADDED) { c = '#'; } if (flags & TINFO_FLAGS_UPDATED) { c = '*'; } AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: [%d]%c %s=%s"), ilabel, c , me->name, me->value); if (ilabelvalue)) { break; } } AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: Tariff changed, now '%s' (%d)"), me->value, tarif); } // Current tariff (standard is in clear text in value) else if (ilabel == LABEL_LTARF) { AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: Tariff name changed, now '%s'"), me->value); } // Current tariff (standard index is is in clear text in value) else if (ilabel == LABEL_NTARF) { tarif = atoi(me->value); AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: Tariff index changed, now '%d'"), tarif); } // Voltage V (not present on all Smart Meter) else if ( ilabel == LABEL_TENSION || ilabel == LABEL_URMS1) { Energy.voltage_available = true; Energy.voltage[0] = (float) atoi(me->value); AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: Voltage %s, now %d"), me->value, (int) Energy.voltage[0]); } // Current I else if (ilabel == LABEL_IINST || ilabel == LABEL_IRMS1) { Energy.current_available = true; Energy.current[0] = (float) atoi(me->value); AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: Current %s, now %d"), me->value, (int) Energy.current[0]); } // Power P else if (ilabel == LABEL_PAPP || ilabel == LABEL_SINSTS) { Energy.active_power[0] = (float) atoi(me->value);; AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: Power %s, now %d"), me->value, (int) Energy.active_power[0]); } // Wh indexes (legacy) else if ( ilabel == LABEL_HCHC || ilabel == LABEL_HCHP || ilabel == LABEL_BASE) { char value[32]; uint32_t hc = 0; uint32_t hp = 0; uint32_t total = 0; // Base, un seul index if (ilabel == LABEL_BASE) { total = atoi(me->value); AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: Base:%u"), total); // Heures creuses/pleines calculer total } else { // Heures creuses get heures pleines if (ilabel == LABEL_HCHC) { hc = atoi(me->value); if ( getValueFromLabelIndex(LABEL_HCHP, value) ) { hp = atoi(value); } // Heures pleines, get heures creuses } else if (ilabel == LABEL_HCHP) { hp = atoi(me->value); if ( getValueFromLabelIndex(LABEL_HCHC, value) ) { hc = atoi(value); } } total = hc + hp; AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: HC:%u HP:%u Total:%u"), hc, hp, total); } EnergyUpdateTotal(total/1000.0f, true); } // Wh total index (standard) else if ( ilabel == LABEL_EAST) { uint32_t total = atoi(me->value); EnergyUpdateTotal(total/1000.0f, true); AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: Total:%uWh"), total); } // Wh indexes (standard) else if ( ilabel == LABEL_EASF01) { AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: HC:%u"), atoi(me->value)); } else if ( ilabel == LABEL_EASF02) { AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: HP:%u"), atoi(me->value)); } // Contract subscribed (legacy) else if (ilabel == LABEL_OPTARIF) { char contrat_value[] = " "; // 4 spaces // Find the contract index for (contrat = CONTRAT_BAS ; contrat < CONTRAT_END ; contrat++) { GetTextIndexed(contrat_value, sizeof(contrat_value), contrat, kContratValue); if (!strcmp(contrat_value, me->value)) { break; } } AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: Contract changed, now '%s' (%d)"), me->value, contrat); } // Contract subscribed (standard is in clear text in value) else if (ilabel == LABEL_NGTF) { AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: Contract changed, now '%s'"), me->value); } // Contract subscribed (Power) else if (ilabel == LABEL_ISOUSC || ilabel == LABEL_PREF) { isousc = atoi( me->value); AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: ISousc set to %d"), isousc); } // Serial Number of device else if (ilabel == LABEL_ADCO || ilabel == LABEL_ADSC) { strcpy(serialNumber, me->value); AddLog(LOG_LEVEL_DEBUG, PSTR("TIC: %s set to %s"), me->name, serialNumber); } } } } /* ====================================================================== Function: responseDumpTInfo Purpose : add teleinfo values into JSON response Input : 1st separator space if begining of JSON, else comma Output : - Comments: - ====================================================================== */ void ResponseAppendTInfo(char sep) { struct _ValueList * me = tinfo.getList(); char * p ; boolean isNumber ; // Loop thru all the teleinfo frame but // always check we don't buffer overflow of MQTT data while (me->next) { // go to next node me = me->next; if (me->name && me->value && *me->name && *me->value) { isNumber = true; p = me->value; // Specific treatment serial number don't convert to number later if (strcmp(me->name, "ADCO")==0 || strcmp(me->name, "ADSC")==0) { isNumber = false; } else { // check if value is number while (*p && isNumber) { if ( *p < '0' || *p > '9' ) { isNumber = false; } p++; } } ResponseAppend_P( PSTR("%c\"%s\":"), sep, me->name ); if (!isNumber) { ResponseAppend_P( PSTR("\"%s\""), me->value ); } else { ResponseAppend_P( PSTR("%d"), atoi(me->value)); } // Now JSON separator is needed sep =','; } } } /* ====================================================================== Function: NewFrameCallback Purpose : callback when we received a complete Teleinfo frama Input : linked list pointer on the concerned data Output : - Comments: - ====================================================================== */ void NewFrameCallback(struct _ValueList * me) { // Reset Energy Watchdog Energy.data_valid[0] = 0; // send teleinfo full frame only if setup like that // see setOption108 if (Settings.flag4.teleinfo_rawdata) { Response_P(PSTR("{")); ResponseAppendTInfo(' '); ResponseJsonEnd(); // Publish adding ADCO serial number into the topic // Need setOption4 to be enabled MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, serialNumber, false); } } /* ====================================================================== Function: TInfoDrvInit Purpose : Tasmota core driver init Input : - Output : - Comments: - ====================================================================== */ void TInfoDrvInit(void) { if (PinUsed(GPIO_TELEINFO_RX)) { TasmotaGlobal.energy_driver = XNRG_15; Energy.voltage_available = false; } } /* ====================================================================== Function: TInfoInit Purpose : Tasmota core device init Input : - Output : - Comments: - ====================================================================== */ void TInfoInit(void) { int baudrate; int serial_buffer_size; // SetOption102 - Set Baud rate for Teleinfo serial communication (0 = 1200 or 1 = 9600) if (Settings.flag4.teleinfo_baudrate) { baudrate = 9600; tinfo_mode = TINFO_MODE_STANDARD; serial_buffer_size = TELEINFO_SERIAL_BUFFER_STANDARD; } else { baudrate = 1200; tinfo_mode = TINFO_MODE_HISTORIQUE; serial_buffer_size = TELEINFO_SERIAL_BUFFER_HISTORIQUE; } if (PinUsed(GPIO_TELEINFO_RX)) { int8_t rx_pin = Pin(GPIO_TELEINFO_RX); AddLog(LOG_LEVEL_INFO, PSTR("TIC: RX on GPIO%d, baudrate %d"), rx_pin, baudrate); // Enable Teleinfo pin used, control it if (PinUsed(GPIO_TELEINFO_ENABLE)) { int8_t en_pin = Pin(GPIO_TELEINFO_ENABLE); pinMode(en_pin, OUTPUT); digitalWrite(en_pin, HIGH); AddLog(LOG_LEVEL_INFO, PSTR("TIC: Enable with GPIO%d"), en_pin); } else { AddLog(LOG_LEVEL_INFO, PSTR("TIC: always enabled")); } #ifdef ESP8266 // Allow GPIO3 AND GPIO13 with hardware fallback to 2 // Set buffer to nnn char to support 250ms loop at 9600 baud TInfoSerial = new TasmotaSerial(rx_pin, -1, 2, 0, serial_buffer_size); //pinMode(rx_pin, INPUT_PULLUP); #endif // ESP8266 #ifdef ESP32 // Set buffer to nnn char to support 250ms loop at 9600 baud TInfoSerial = new TasmotaSerial(rx_pin, -1, 1, 0, serial_buffer_size); #endif // ESP32 // Trick here even using SERIAL_7E1 or TS_SERIAL_7E1 // this is not working, need to call SetSerialConfig after if (TInfoSerial->begin(baudrate)) { #ifdef ESP8266 if (TInfoSerial->hardwareSerial() ) { ClaimSerial(); // This is a hack, looks like begin does not take into account // the TS_SERIAL_7E1 configuration so on ESP8266 this is // working only on Serial RX pin (Hardware Serial) for now //SetSerialConfig(TS_SERIAL_7E1); //TInfoSerial->setTimeout(TINFO_READ_TIMEOUT); AddLog(LOG_LEVEL_INFO, PSTR("TIC: using hardware serial")); } else { AddLog(LOG_LEVEL_INFO, PSTR("TIC: using software serial")); } #endif // ESP8266 #ifdef ESP32 SetSerialConfig(TS_SERIAL_7E1); AddLog(LOG_LEVEL_INFO, PSTR("TIC: using ESP32 hardware serial")); #endif // ESP32 // Init teleinfo tinfo.init(tinfo_mode); // Attach needed callbacks tinfo.attachADPS(ADPSCallback); tinfo.attachData(DataCallback); tinfo.attachNewFrame(NewFrameCallback); tinfo_found = true; AddLog(LOG_LEVEL_INFO, PSTR("TIC: Ready")); } } } /* ====================================================================== Function: TInfoProcess Purpose : Tasmota callback executed often enough to read serial Input : - Output : - Comments: - ====================================================================== */ //#define MEASURE_PERF // Define to enable performance measurments void TInfoProcess(void) { static char buff[TELEINFO_PROCESS_BUFFER]; #ifdef MEASURE_PERF static unsigned long max_time = 0; unsigned long duration = millis(); static int max_size = 0; int tmp_size = 0; #endif if (!tinfo_found) { return; } int size = TInfoSerial->read(buff,TELEINFO_PROCESS_BUFFER); while ( size ) { #ifdef MEASURE_PERF tmp_size += size; #endif // Process as many bytes as available in serial buffer for(int i = 0; size; i++, size--) { buff[i] &= 0x7F; // data processing tinfo.process(buff[i]); } size = TInfoSerial->read(buff,TELEINFO_PROCESS_BUFFER); } #ifdef MEASURE_PERF duration = millis()-duration; if (duration > max_time) { max_time = duration; AddLog(LOG_LEVEL_INFO,PSTR("TIC: max_time=%lu"), max_time); } if (tmp_size > max_size) { max_size = tmp_size; AddLog(LOG_LEVEL_INFO,PSTR("TIC: max_size=%d"), max_size); } #endif } /* ====================================================================== Function: TInfoShow Purpose : Tasmota callback executed to send telemetry or WEB display Input : - Output : - Comments: - ====================================================================== */ #ifdef USE_WEBSERVER const char HTTP_ENERGY_ID_TELEINFO[] PROGMEM = "{s}ID{m}%s{e}" ; const char HTTP_ENERGY_INDEX_TELEINFO[] PROGMEM = "{s}%s{m}%s " D_UNIT_WATTHOUR "{e}" ; const char HTTP_ENERGY_PAPP_TELEINFO[] PROGMEM = "{s}" D_POWERUSAGE "{m}%d " D_UNIT_WATT "{e}" ; const char HTTP_ENERGY_IINST_TELEINFO[] PROGMEM = "{s}" D_CURRENT "{m}%d " D_UNIT_AMPERE "{e}" ; const char HTTP_ENERGY_TARIF_TELEINFO[] PROGMEM = "{s}" D_CURRENT_TARIFF "{m}Heures %s{e}" ; const char HTTP_ENERGY_CONTRAT_TELEINFO[] PROGMEM = "{s}" D_CONTRACT "{m}%s %d" D_UNIT_AMPERE "{e}" ; const char HTTP_ENERGY_LOAD_TELEINFO[] PROGMEM = "{s}" D_POWER_LOAD "{m}%d" D_UNIT_PERCENT "{e}" ; const char HTTP_ENERGY_IMAX_TELEINFO[] PROGMEM = "{s}" D_MAX_CURRENT "{m}%d" D_UNIT_AMPERE "{e}" ; const char HTTP_ENERGY_PMAX_TELEINFO[] PROGMEM = "{s}" D_MAX_POWER "{m}%d" D_UNIT_WATT "{e}" ; #endif // USE_WEBSERVER void TInfoShow(bool json) { // Since it's an Energy device , current, voltage and power are // already present on the telemetry frame. No need to add here // Just add the raw label/values of the teleinfo frame if (json) { // Calculated values if (isousc) { ResponseAppend_P(PSTR(",\"Load\":%d"),(int) ((Energy.current[0]*100.0f) / isousc)); } // add teleinfo full frame ResponseAppendTInfo(','); #ifdef USE_WEBSERVER } else { char name[32]; char value[32]; if (getValueFromLabelIndex(LABEL_BASE, value) ) { GetTextIndexed(name, sizeof(name), LABEL_BASE, kLabel); WSContentSend_PD(HTTP_ENERGY_INDEX_TELEINFO, name, value); } if (getValueFromLabelIndex(LABEL_HCHC, value) ) { GetTextIndexed(name, sizeof(name), LABEL_HCHC, kLabel); WSContentSend_PD(HTTP_ENERGY_INDEX_TELEINFO, name, value); } if (getValueFromLabelIndex(LABEL_HCHP, value) ) { GetTextIndexed(name, sizeof(name), LABEL_HCHP, kLabel); WSContentSend_PD(HTTP_ENERGY_INDEX_TELEINFO, name, value); } if (getValueFromLabelIndex(LABEL_IMAX, value) ) { WSContentSend_PD(HTTP_ENERGY_IMAX_TELEINFO, atoi(value)); } if (getValueFromLabelIndex(LABEL_PMAX, value) ) { WSContentSend_PD(HTTP_ENERGY_PMAX_TELEINFO, atoi(value)); } if (tinfo_mode==TINFO_MODE_HISTORIQUE ) { if (tarif) { GetTextIndexed(name, sizeof(name), tarif-1, kTarifName); WSContentSend_PD(HTTP_ENERGY_TARIF_TELEINFO, name); } if (contrat && isousc) { int percent = (int) ((Energy.current[0]*100.0f) / isousc) ; GetTextIndexed(name, sizeof(name), contrat, kContratName); WSContentSend_PD(HTTP_ENERGY_CONTRAT_TELEINFO, name, isousc); WSContentSend_PD(HTTP_ENERGY_LOAD_TELEINFO, percent); } } else if (tinfo_mode==TINFO_MODE_STANDARD ) { if (getValueFromLabelIndex(LABEL_LTARF, name) ) { WSContentSend_PD(HTTP_ENERGY_TARIF_TELEINFO, name); } if (getValueFromLabelIndex(LABEL_NGTF, name) ) { if (isousc) { int percent = (int) ((Energy.current[0]*100.0f) / isousc) ; WSContentSend_PD(HTTP_ENERGY_CONTRAT_TELEINFO, name, isousc); WSContentSend_PD(HTTP_ENERGY_LOAD_TELEINFO, percent); } } } // Serial number ADCO or ADSC if found if (*serialNumber) { WSContentSend_PD(HTTP_ENERGY_ID_TELEINFO, serialNumber); } #endif // USE_WEBSERVER } } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xnrg15(uint8_t function) { switch (function) { case FUNC_EVERY_250_MSECOND: TInfoProcess(); break; case FUNC_JSON_APPEND: TInfoShow(1); break; #ifdef USE_WEBSERVER case FUNC_WEB_SENSOR: TInfoShow(0); break; #endif // USE_WEBSERVER case FUNC_INIT: TInfoInit(); break; case FUNC_PRE_INIT: TInfoDrvInit(); break; } return false; } #endif // USE_TELEINFO #endif // USE_ENERGY_SENSOR