/*
xnrg_15_Teleinfo.ino - Teleinfo support for Tasmota
Copyright (C) 2020 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/
*
* Hardware Serial will be selected if GPIO1 = [TELEINFO_RX]
\*********************************************************************************************/
#define XNRG_15 15
#include "LibTeleinfo.h"
#include
#define TINFO_READ_TIMEOUT 400
enum TInfoContrat{
CONTRAT_BAS = 1, // BASE => Option Base.
CONTRAT_HC, // HC.. => Option Heures Creuses.
CONTRAT_EJP, // EJP. => Option EJP.
CONTRAT_BBR // BBRx => Option Tempo
};
enum TInfoTarif{
TARIF_TH = 1, // Toutes les Heures.
TARIF_HC, // Heures Creuses.
TARIF_HP, // Heures Pleines.
TARIF_HN, // BBRx => Option Tempo
TARIF_PM, // Heures de Pointe Mobile.
TARIF_CB, // Heures Creuses Jours Bleus.
TARIF_CW, // Heures Creuses Jours Blancs (White).
TARIF_CR, // Heures Creuses Jours Rouges.
TARIF_PB, // Heures Pleines Jours Bleus.
TARIF_PW, // Heures Pleines Jours Blancs (White).
TARIF_PR // Heures Pleines Jours Rouges.
};
// Label received
const char LABEL_HCHC[] PROGMEM = "HCHC";
const char LABEL_HCHP[] PROGMEM = "HCHP";
const char LABEL_PTEC[] PROGMEM = "PTEC";
const char LABEL_PAPP[] PROGMEM = "PAPP";
const char LABEL_IINST[] PROGMEM = "IINST";
const char LABEL_TENSION[] PROGMEM = "TENSION";
// Some Values with string to compare to
const char VALUE_HCDD[] PROGMEM = "HC..";
const char kTARIF_TH[] PROGMEM = "Toutes";
const char kTARIF_HC[] PROGMEM = "Creuses";
const char kTARIF_HP[] PROGMEM = "Pleines";
const char kTARIF_HN[] PROGMEM = "Normales";
const char kTARIF_PM[] PROGMEM = "Pointe Mobile";
const char kTARIF_CB[] PROGMEM = "Creuses Bleu";
const char kTARIF_CW[] PROGMEM = "Creuses Blanc";
const char kTARIF_CR[] PROGMEM = "Creuses Rouge";
const char kTARIF_PB[] PROGMEM = "Pleines Bleu";
const char kTARIF_PW[] PROGMEM = "Pleines Blanc";
const char kTARIF_PR[] PROGMEM = "Pleines Rouge";
const char * kTtarifNames[] PROGMEM = {
kTARIF_TH,
kTARIF_HC, kTARIF_HP,
kTARIF_HN, kTARIF_PM,
kTARIF_CB, kTARIF_CW, kTARIF_CR, kTARIF_PB, kTARIF_PW, kTARIF_PR
};
TInfo tinfo; // Teleinfo object
TasmotaSerial *TInfoSerial = nullptr;
bool tinfo_found = false;
uint8_t contrat;
uint8_t tarif;
/*********************************************************************************************/
/* ======================================================================
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 in the main sketch with a
tinfo.attachADPSCallback(ADPSCallback())
====================================================================== */
void ADPSCallback(uint8_t phase)
{
// n = phase number 1 to 3
if (phase == 0)
phase = 1;
AddLog_P2(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 = ' ';
// Does this value is new or changed?
if (flags & (TINFO_FLAGS_ADDED | TINFO_FLAGS_UPDATED) )
{
if (flags & TINFO_FLAGS_ADDED) { c = '#'; }
if (flags & TINFO_FLAGS_UPDATED) { c = '*'; }
// Current tarif
if (!strcmp_P(LABEL_PTEC, me->name))
{
if (!strcmp_P("TH..", me->name)) { tarif = TARIF_TH; }
else if (!strcmp_P("HC..", me->name)) { tarif = TARIF_HC; }
else if (!strcmp_P("HP..", me->name)) { tarif = TARIF_HP; }
else if (!strcmp_P("HN..", me->name)) { tarif = TARIF_HN; }
else if (!strcmp_P("PM..", me->name)) { tarif = TARIF_PM; }
else if (!strcmp_P("HCJB", me->name)) { tarif = TARIF_CB; }
else if (!strcmp_P("HCJW", me->name)) { tarif = TARIF_CW; }
else if (!strcmp_P("HCJR", me->name)) { tarif = TARIF_CR; }
else if (!strcmp_P("HPJB", me->name)) { tarif = TARIF_PB; }
else if (!strcmp_P("HPJW", me->name)) { tarif = TARIF_PW; }
else if (!strcmp_P("HPJR", me->name)) { tarif = TARIF_PR; }
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("TIC: Tarif changed, now '%s' (%d)"), me->value, tarif);
}
// Voltage V (not present on all Smart Meter)
else if (!strcmp_P(LABEL_TENSION, me->name))
{
Energy.voltage_available = true;
int i = atoi(me->value);
Energy.voltage[0] = (float) atoi(me->value);
// Update current
if (Energy.voltage_available && Energy.voltage[0]) {
Energy.current[0] = Energy.active_power[0] / Energy.voltage[0] ;
}
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("TIC: Voltage %s, now %d"), me->value, i);
}
// Current I
else if (!strcmp_P(LABEL_IINST, me->name))
{
if (!Energy.voltage_available) {
int i = atoi(me->value);
Energy.current[0] = (float) atoi(me->value);
} else if (Energy.voltage[0]) {
Energy.current[0] = Energy.active_power[0] / Energy.voltage[0] ;
}
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("TIC: Current %s, now %d"), me->value, (int) Energy.current[0]);
}
// Current P
else if (!strcmp_P(LABEL_PAPP, me->name))
{
int papp = atoi(me->value);
Energy.active_power[0] = (float) atoi(me->value);
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("TIC: Power %s, now %d"), me->value, papp);
// Update current
if (Energy.voltage_available && Energy.voltage[0]) {
Energy.current[0] = Energy.active_power[0] / Energy.voltage[0] ;
}
}
// kWh indexes
else if (!strcmp_P(LABEL_HCHC, me->name) || !strcmp(LABEL_HCHP, me->name))
{
char value[32];
unsigned long hc = 0;
unsigned long hp = 0;
unsigned long total = 0;
if ( tinfo.valueGet_P(LABEL_HCHC, value) ) { hc = atol(value);}
if ( tinfo.valueGet_P(LABEL_HCHP, value) ) { hp = atol(value);}
total = hc+hp;
EnergyUpdateTotal(total/1000.0f, true);
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("TIC: HC:%ld HP:%ld Total:%ld"), hc, hp, total);
}
}
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("TIC: %c %s=%s"),c , me->name, me->value);
}
/* ======================================================================
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;
}
/* ======================================================================
Function: NewFrameCallback
Purpose : callback when we received a complete Teleinfo frama
Input : label to search for
Output : value filled
Comments: -
====================================================================== */
char * getDataValue_P(const char * label, char * value)
{
if (!tinfo.valueGet_P(label, value) ) {
*value = '\0';
}
return value;
}
void TInfoDrvInit(void) {
if (PinUsed(GPIO_TELEINFO_RX)) {
energy_flg = XNRG_15;
Energy.voltage_available = false;
//Energy.current_available = false;
Energy.type_dc = true;
}
}
void TInfoInit(void)
{
#ifdef USE_TELEINFO_STANDARD
#define TINFO_SPEED 9600
#else
#define TINFO_SPEED 1200
#endif
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("TIC: inferface speed %d bps"),TINFO_SPEED);
if (PinUsed(GPIO_TELEINFO_RX))
{
uint8_t rx_pin = Pin(GPIO_TELEINFO_RX);
AddLog_P2(LOG_LEVEL_INFO, PSTR("TIC: RX on GPIO%d"), rx_pin);
// Enable Teleinfo
if (PinUsed(GPIO_TELEINFO_ENABLE))
{
uint8_t en_pin = Pin(GPIO_TELEINFO_ENABLE);
pinMode(en_pin, OUTPUT);
digitalWrite(en_pin, HIGH);
AddLog_P2(LOG_LEVEL_INFO, PSTR("TIC: Enable with GPIO%d"), en_pin);
}
else
{
AddLog_P2(LOG_LEVEL_INFO, PSTR("TIC: always enabled"));
}
TInfoSerial = new TasmotaSerial(rx_pin, -1, 1);
// pinMode(GPIO_TELEINFO_RX, INPUT_PULLUP);
// Trick here even using SERIAL_7E1 or TS_SERIAL_7E1
// this is not working, need to call SetSerialConfig after
if (TInfoSerial->begin(TINFO_SPEED))
{
#if defined (ESP8266)
if (TInfoSerial->hardwareSerial() ) {
ClaimSerial();
AddLog_P2(LOG_LEVEL_INFO, PSTR("TIC: using hardware serial"));
} else {
AddLog_P2(LOG_LEVEL_INFO, PSTR("TIC: using software serial"));
}
// This is a dirty 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);
#elif defined (ESP32)
AddLog_P2(LOG_LEVEL_INFO, PSTR("TIC: using ESP32 hardware serial"));
// Waiting TasmotaSerial PR merged to change that
//TInfoSerial->reconf(TINFO_SPEED, SERIAL_7E1);
#endif
// Init teleinfo
tinfo.init();
// Attach needed callbacks
tinfo.attachADPS(ADPSCallback);
tinfo.attachData(DataCallback);
tinfo.attachNewFrame(NewFrameCallback);
tinfo_found = true;
AddLog_P2(LOG_LEVEL_INFO, PSTR("TIC: Ready"));
}
}
}
void TInfoLoop(void)
{
char c;
if (!tinfo_found)
return;
if (TInfoSerial->available()) {
//AddLog_P2(LOG_LEVEL_INFO, PSTR("TIC: received %d chars"), TInfoSerial->available());
// We received some data?
while (TInfoSerial->available()>8)
{
// get char
c = TInfoSerial->read();
// data processing
tinfo.process(c);
}
}
}
void TInfoEvery250ms(void)
{
}
#ifdef USE_WEBSERVER
const char HTTP_ENERGY_INDEX_TELEINFO[] PROGMEM = "{s}%s{m}%s " D_UNIT_KILOWATTHOUR "{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}Tarif{m}%s{e}" ;
#endif // USE_WEBSERVER
void TInfoShow(bool json)
{
char value[32];
// TBD
if (json)
{
if ( tinfo.valueGet_P(LABEL_PTEC, value) ) {
ResponseAppend_P(PSTR(",\"" "TARIF" "\":%s"), value);
}
if ( tinfo.valueGet_P(LABEL_IINST, value) ) {
ResponseAppend_P(PSTR(",\"" D_CURRENT "\":%s"), value);
}
if ( tinfo.valueGet_P(LABEL_PAPP, value) ) {
ResponseAppend_P(PSTR(",\"" D_POWERUSAGE "\":%s"), value);
}
if ( tinfo.valueGet_P(LABEL_HCHC, value) ) {
ResponseAppend_P(PSTR(",\"" "HC" "\":%s"), value);
}
if ( tinfo.valueGet_P(LABEL_HCHP, value) ) {
ResponseAppend_P(PSTR(",\"" "HP" "\":%s"), value);
}
#ifdef USE_WEBSERVER
}
else
{
getDataValue_P(LABEL_HCHC, value);
WSContentSend_PD(HTTP_ENERGY_INDEX_TELEINFO, kTARIF_HC, value);
getDataValue_P(LABEL_HCHP, value);
WSContentSend_PD(HTTP_ENERGY_INDEX_TELEINFO, kTARIF_HP, value);
if (tarif) {
WSContentSend_PD(HTTP_ENERGY_TARIF_TELEINFO, kTtarifNames[tarif-1]);
}
#endif // USE_WEBSERVER
}
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
bool Xnrg15(uint8_t function)
{
switch (function)
{
case FUNC_LOOP:
if (TInfoSerial) { TInfoLoop(); }
break;
case FUNC_EVERY_250_MSECOND:
if (uptime > 4) { TInfoEvery250ms(); }
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