diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4ed84e3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + platformio: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cache/pip + ~/.platformio/.cache + key: ${{ runner.os }}-pio + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install PlatformIO + run: python -m pip install --upgrade pip platformio + - name: Build + run: pio run diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a37043c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +src/ConfigUser.h diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5e737fc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "stm32-for-vscode.openOCDPath": false, + "stm32-for-vscode.armToolchainPath": false +} \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..98b40ba --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,3 @@ +This Source Code Form is subject to the terms of the Mozilla Public License, +v. 2.0. If a copy of the MPL was not distributed with this file, You can +obtain one at https://mozilla.org/MPL/2.0/. diff --git a/README.md b/README.md index 0de8055..5ee84e5 100644 --- a/README.md +++ b/README.md @@ -1 +1,113 @@ -# linkyToVictron +# linkyToVictron OLED + +Firmware ESP8266 (NodeMCU v2) qui lit la teleinfo d'un compteur Linky triphase et affiche les donnees en temps reel sur un ecran OLED SSD1306. + +## Cablage + +| Composant | Broche | +|-----------------|--------| +| OLED SDA | D1 | +| OLED SCL | D2 | +| Teleinfo RX | D7 | +| OLED I2C addr | 0x3C | + +## Fonctionnalites + +- **Auto-detection TIC** : bascule automatiquement entre mode Standard (9600 bauds) et Historique (1200 bauds) +- **Triphase** : courant, tension et puissance par phase +- **Injection solaire** : gestion des puissances negatives (SINSTI, EAIT) +- **Index** : HC, HP, injection, total en kWh +- **Affichage OLED** : 3 pages en alternance (temps reel / index energie / reseau) +- **MQTT Victron** : publication au format `Ac/*` compatible `dbus-mqtt-grid` + +## Structure + +```text +src/ + Config.h -- Configuration par defaut (sans secrets) + ConfigUser.example.h -- Exemple de config locale + main.cpp -- Setup/loop, instanciation des objets + TeleinfoData.h -- Struct de donnees teleinfo + TeleinfoReader.h/.cpp -- Lecture serie + parsing des trames TIC + WifiManager.h/.cpp -- Connexion Wi-Fi + MqttPublisher.h/.cpp -- Publication MQTT + OledDisplay.h/.cpp -- Affichage OLED SSD1306 +platformio.ini -- Configuration PlatformIO +``` + +## Configuration locale + +Creer une configuration locale non versionnee : + +```bash +cp src/ConfigUser.example.h src/ConfigUser.h +``` + +Puis editer `src/ConfigUser.h` avec vos identifiants Wi-Fi et MQTT. + +## Compilation + +```bash +pio run +``` + +## Upload + +```bash +pio run --target upload +``` + +## Dependances + +- [Adafruit GFX Library](https://github.com/adafruit/Adafruit-GFX-Library) +- [Adafruit SSD1306](https://github.com/adafruit/Adafruit_SSD1306) +- [LibTeleinfo](https://github.com/hallard/LibTeleinfo) +- [PubSubClient](https://github.com/knolleary/pubsubclient) + + + + + + + + + + + + + + + + + + + + + + + + +## Audit & Execution Plan (2026-03-10) + +### Snapshot +- Priority: `P2` +- Tech profile: `embedded` +- Workflows: `yes` +- Tests: `yes` +- Debt markers: `0` +- Source files: `12` + +### Corrections Prioritaires +- [ ] Vérifier target PlatformIO et budget mémoire +- [ ] Ajouter/fiabiliser les commandes de vérification automatiques. +- [ ] Clore les points bloquants avant optimisation avancée. + +### Optimisation +- [ ] Identifier le hotspot principal et mesurer avant/après. +- [ ] Réduire la complexité des modules les plus touchés. + +### Mémoire chantier +- Control plane: `/Users/electron/.codex/memories/electron_rare_chantier` +- Repo card: `/Users/electron/.codex/memories/electron_rare_chantier/REPOS/linkyToVictron_oled.md` + + diff --git a/compteur.cpp b/compteur.cpp deleted file mode 100644 index a99f0e8..0000000 --- a/compteur.cpp +++ /dev/null @@ -1,336 +0,0 @@ -#include "compteur.h" -#include -#include - - - - - - -Compteur::Compteur(QObject *parent) - : QObject(parent), serial(this), timerReadData(this),energyMeter(parent) -{ - serial.setPortName("domotiqueCompteur"); - serial.setBaudRate(9600); - serial.setDataBits(QSerialPort::Data7); - serial.setParity(QSerialPort::EvenParity); - retryOpenSerial.setInterval(500); - retryOpenSerial.setSingleShot(true); - timerReadData.setInterval(400); - - connect(&retryOpenSerial,&QTimer::timeout,this,&Compteur::serialError); - connect(&timerReadData,&QTimer::timeout,this,&Compteur::getData); - //connect(&serial,&QSerialPort::readyRead,this,&Compteur::getData); - - - connect(&m_client,SIGNAL(connected()),this,SLOT(mqttConnected())); - connect(&m_client,SIGNAL(disconnected()),this,SLOT(mqttDisConnected())); - connect(&retryMqttTimer,SIGNAL(timeout()),this,SLOT(mqttReconnect())); - -/* listeEtiquette.append("EASF01"); - listeTopic.append("fluide/elec/hc"); - listeValeur.append(""); - listeNewValeur.append(""); - listeDiv.append(100); - listeArrondi.append(0); - - listeEtiquette.append("EASF02"); - listeTopic.append("fluide/elec/hp"); - listeValeur.append(""); - listeNewValeur.append(""); - listeDiv.append(100); - listeArrondi.append(0); -*/ - listeEtiquette.append("PTEC"); - listeTopic.append("fluide/elec/tarif"); - listeValeur.append(""); - listeNewValeur.append(""); - listeDiv.append(0); - listeArrondi.append(0); - - - nbEtiquettes=listeEtiquette.size(); - - m_client.setHostname(QString("iot")); - m_client.setPort(1883); - - retryMqttTimer.setInterval(100); - retryMqttTimer.start(); - souscription=nullptr; - - - retryOpenSerial.start(); -} - -void Compteur::serialError() -{ - disconnect(&serial,&QSerialPort::errorOccurred,this,&Compteur::serialError); - timerReadData.stop(); - qDebug()<<"tentative d'ouverture de la liaison serie"; - serial.close(); - if (serial.open(QIODevice::ReadOnly)==false) - { - - qDebug()<<"echec"; - retryOpenSerial.start(); - serial.close(); - } - else - { - retryOpenSerial.stop(); - qDebug()<<"ok"; - connect(&serial,&QSerialPort::errorOccurred,this,&Compteur::serialError); - timerReadData.start(); - } - -} - -void Compteur::getData() -{ - QString line; - static bool start=false; - bool run=true; - char c='\0',pc; - char crc=0; - do - { - pc=c; - if (serial.getChar(&c)) - { - if (c=='\n') - { - line.clear(); - crc=0; - } - else if (c=='\r') - { - crc-=pc; - crc=(crc&0x3F)+0x20; - QStringList liste = line.split(QLatin1Char('\t')); - //qDebug()<0) iInst=-iInst; - pInst=-pInj; - } - else - { - if (iInst<0) iInst=-iInst; - pInst=pSout; - } - if (iInst!=iInstPrec) - { - iInstPrec=iInst; - //qDebug()<<"topic"<<"fluide/elec/iinst"<=0) - { - listeNewValeur[i] = liste.at(1); - int div=listeDiv.at(i); - if (div!=0) listeNewValeur[i]=QString::number((listeNewValeur[i].toInt()/div)); - int arrondi=listeArrondi.at(i); - if (arrondi!=0) listeNewValeur[i]=QString::number(( ((listeNewValeur[i].toInt())/arrondi)*arrondi)); - } - } - } - } - - else - { - line.append(c); - crc+=c; - } - - if (serial.bytesAvailable()<20) run=false; - } - else - { - run=false; - } - }while(run); - -} - -void Compteur::addHomeAssistant(QString name, QString type, QString unit) -{ - QString payload; - payload.append("{"); - if (type!="") payload.append("\"device_class\": \""+type+"\","); - payload.append("\"state_topic\": \"homeassistant/sensor/teleinfo"+name+"/state\","); - payload.append("\"name\": \"teleinfo"+name+"\","); - payload.append("\"unique_id\": \"teleinfo"+name+"\","); - if (unit!="") payload.append("\"unit_of_measurement\": \""+unit+"\"}"); - emit mqttSend("homeassistant/sensor/teleinfo"+name+"/config", - payload, - 1,1); -} - - -void Compteur::mqttConnected() -{ - isConnected = true; - retryMqttTimer.stop(); - souscription=m_client.subscribe(QMqttTopicFilter("fluide/elec/pSolaire"),1); - connect(souscription,&QMqttSubscription::messageReceived, - this,&Compteur::newPuissanceSolaire); - - addHomeAssistant("Pinst","power","W"); - addHomeAssistant("Pconso","power","W"); - addHomeAssistant("IndexHC","energy","kWh"); - addHomeAssistant("IndexHP","energy","kWh"); - addHomeAssistant("IndexInjection","energy","kWh"); - addHomeAssistant("IndexHP","energy","kWh"); - addHomeAssistant("IndexHC","energy","kWh"); - - qDebug()<<"connect to Mosquitto !"; -} - -void Compteur::mqttDisConnected() -{ - isConnected = false; - if (souscription!=nullptr) - { - disconnect(souscription,&QMqttSubscription::messageReceived, - this,&Compteur::newPuissanceSolaire); - delete souscription; - souscription=nullptr; - } - qDebug()<<"connection au serveur perdue !!!"; - retryMqttTimer.start(); -} - -void Compteur::mqttReconnect() -{ - qDebug()<<"try to (re)connect !"; - m_client.connectToHost(); -} - - -void Compteur::mqttSend(QString topic, QString message, int qos, bool retain) -{ - if (isConnected) - { - m_client.publish(topic, message.toLatin1(),quint8(qos),retain); - } -} - -void Compteur::newPuissanceSolaire(QMqttMessage message) -{ - //qDebug()<<"new message : "< -#include -#include -#include -#include - -#include "qem24energymeter.h" - -class Compteur : public QObject -{ - Q_OBJECT -public: - explicit Compteur(QObject *parent = nullptr); - -private: - QSerialPort serial; - QTimer retryOpenSerial; - QTimer timerReadData; - - bool isConnected; - QMqttClient m_client; - QTimer retryMqttTimer; - QMqttSubscription * souscription; - - - QStringList listeEtiquette; - QStringList listeValeur; - QStringList listeNewValeur; - QStringList listeTopic; - QList listeDiv; - QList listeArrondi; - - - QEM24EnergyMeter energyMeter; - bool isEnergyMeterStarted=false; - - - int nbEtiquettes; - - int pSout; - int pInj; - int pInst; - int pInstPrec=-9999; - int pConso; - int pConsoPrec=-9999; - int iInst; - int iInstPrec=-9999; - int uInst=230; - int uInstPrec=0; - long indexInj; - long indexInjPrec=-9999; - int pSolaire=0; - bool injection; - int64_t indexHC=0; - int64_t indexHCprec=0; - int64_t indexHP=0; - int64_t indexHPprec=0; - int64_t indexTotal=0; - int64_t indexTotalprec=0; - - - -signals: - -public slots: - void serialError(); - void getData(); - - void mqttConnected(); - void mqttDisConnected(); - void mqttReconnect(); - void mqttSend(QString topic, QString message,int qos,bool retain); - void newPuissanceSolaire(QMqttMessage message); - void addHomeAssistant(QString name, QString type, QString unit); - -}; - -#endif // COMPTEUR_H diff --git a/main.cpp b/main.cpp deleted file mode 100644 index d584ef8..0000000 --- a/main.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include -#include "compteur.h" - -int main(int argc, char *argv[]) -{ - QCoreApplication a(argc, argv); - Compteur cpt(&a); - return a.exec(); -} diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..f9a1b2d --- /dev/null +++ b/platformio.ini @@ -0,0 +1,10 @@ +[env:nodemcuv2] +platform = espressif8266 +board = nodemcuv2 +framework = arduino +monitor_speed = 9600 +lib_deps = + adafruit/Adafruit GFX Library@^1.12.1 + adafruit/Adafruit SSD1306@^2.5.15 + https://github.com/hallard/LibTeleinfo.git + knolleary/PubSubClient@^2.8 diff --git a/qem24energymeter.cpp b/qem24energymeter.cpp deleted file mode 100644 index d2c876c..0000000 --- a/qem24energymeter.cpp +++ /dev/null @@ -1,216 +0,0 @@ -#include "qem24energymeter.h" -#include - - -QEM24EnergyMeter::QEM24EnergyMeter(QObject *parent) - : QObject{parent} -{ -// reg.insert(QModbusDataUnit::Coils, { QModbusDataUnit::Coils, 0, 10 }); -// reg.insert(QModbusDataUnit::DiscreteInputs, { QModbusDataUnit::DiscreteInputs, 0, 10 }); - reg.insert(QModbusDataUnit::InputRegisters, { QModbusDataUnit::InputRegisters, 0, 0xA101 }); - reg.insert(QModbusDataUnit::HoldingRegisters, { QModbusDataUnit::HoldingRegisters, 0, 0xA101 }); - - - modbusDevice.setMap(reg); - // 000Bh Carlo Gavazzi identification code 1648 - - setRegistre(0xB,1648); - setRegistre( 0x0302, 0x101E ); // Version and revision code of measurement module - setRegistre( 0x0304, 0x101E ); // Version and revision code of communication module - setRegistre( 0x1002, 3 ); // Measuring system (3="1Ph", 4=“3P”) - //setRegistre( 0x1003, 10 ); // current transformer ratio) - //setRegistre( 0x1005, 10 ); // tension transformer ratio - //setRegistre( 0x1010, 15 ); // interval Time - - //setRegistre( 0x1109, 1 ); // filter coeff - - setRegistre(0x2100,192); - setRegistre(0x2101,168); - setRegistre(0x2102,0); - setRegistre(0x2103,7); - - setRegistre(0x2104,255); - setRegistre(0x2105,255); - setRegistre(0x2106,255); - setRegistre(0x2107,0); - - setRegistre(0x2108,192); - setRegistre(0x2109,168); - setRegistre(0x210A,0); - setRegistre(0x210B,1); - - setRegistre(0x210C,502); - - setRegistre(0x2120,192); - setRegistre(0x2121,168); - setRegistre(0x2122,0); - setRegistre(0x2123,7); - - setRegistre(0x2124,255); - setRegistre(0x2125,255); - setRegistre(0x2126,255); - setRegistre(0x2127,0); - - setRegistre(0x2128,192); - setRegistre(0x2129,168); - setRegistre(0x212A,0); - setRegistre(0x212B,1); - - - setRegistre( 0x0032, 0 ); // Phase sequence - setRegistre( 0x0033, 500 ); // Frequence - setRegistre( 0xA100, 3 ); //Front selector status lock - setRegistre( 0x5000, 0x4c69 ); //Serial Digit 1 & 2 - setRegistre( 0x5001, 0x6e6b ); //Serial Digit 3 & 4 - setRegistre( 0x5002, 0x7930 ); //Serial Digit 5 & 6 - setRegistre( 0x5003, 0x3030 ); //Serial Digit 7 & 8 - setRegistre( 0x5004, 0x3030 ); //Serial Digit 9 & 10 - setRegistre( 0x5005, 0x3030 ); //Serial Digit 11 & 12 - setRegistre( 0x5006, 0x3100 ); //Serial Digit 13 - setRegistre( 0xA000, 7 ); //Application : Doit être égal à 7 pour Victron - //Infos triphasées non utilisées: - setRegistre( 0x0001, 0 ); - setRegistre( 0x0002, 0 ); // V-L3 - setRegistre( 0x0003, 0 ); - setRegistre( 0x0004, 0 ); // V-L2 - setRegistre( 0x0005, 0 ); - setRegistre( 0x000C, 100 ); // A-L1 - setRegistre( 0x000D, 0 ); - setRegistre( 0x000E, 0 ); // A-L1 - setRegistre( 0x000F, 0 ); - setRegistre( 0x0010, 0 ); // A-L2 - setRegistre( 0x0011, 0 ); - setRegistre( 0x0013, 0 ); - setRegistre( 0x0016, 0 ); // W-L2 - setRegistre( 0x0017, 0 ); - setRegistre( 0x0019, 0 ); - setRegistre( 0x0042, 0 ); // kWh-L2 - setRegistre( 0x0043, 0 ); - setRegistre( 0x0028, 1000 ); // W-Total - setRegistre( 0x0029, 0 ); - setRegistre( 0x002A, 1000 ); // VA-Total - setRegistre( 0x002B, 0 ); - setRegistre( 0x0034, 100); // kWh + - setRegistre( 0x0035, 0 ); - //Initialisation des valeurs pertinentes pour le monophasé - setRegistre( 0x0040, 1 ); // kWh-L1 - setRegistre( 0x0041, 0 ); - setRegistre( 0x0044, 0 ); // kWh-L2 - setRegistre( 0x0045, 0 ); - setRegistre( 0x004E, 0 ); // kWh- - setRegistre( 0x004F, 0 ); - setRegistre( 0x0046, 0 ); // kWh+ T1 - setRegistre( 0x0047, 0 ); - setRegistre( 0x0048, 0 ); // kWh+ T2 - setRegistre( 0x0049, 0 ); - - setPuissanceInstantanee(0); - setTension(0); - - - - modbusDevice.setConnectionParameter(QModbusDevice::NetworkPortParameter, 502); - modbusDevice.setConnectionParameter(QModbusDevice::NetworkAddressParameter, "192.168.0.7"); - modbusDevice.setServerAddress(1); - modbusDevice.setValue(QModbusServer::ListenOnlyMode, 0); - //modbusDevice.setValue(QModbusServer::DeviceBusy, 0xfff); - - - //connect(&modbusDevice, &QModbusTcpServer::dataWritten, - // this, &QEM24EnergyMeter::updateWidgets); - connect(&modbusDevice, &QModbusTcpServer::stateChanged, - this, &QEM24EnergyMeter::onStateChanged); - connect(&modbusDevice, &QModbusTcpServer::errorOccurred, - this, &QEM24EnergyMeter::handleDeviceError); - - connect(&modbusDevice, &QModbusTcpServer::modbusClientDisconnected, - this, &QEM24EnergyMeter::clientDisconnected); - - modbusDevice.setData(QModbusDataUnit::Coils, 0, 1); - modbusDevice.setData(QModbusDataUnit::Coils, 1, 2); - - - -} - -void QEM24EnergyMeter::setRegistre(quint16 address, quint16 value) -{ - modbusDevice.setData(QModbusDataUnit::HoldingRegisters,address,value); - modbusDevice.setData(QModbusDataUnit::InputRegisters,address,value); -} - -void QEM24EnergyMeter::setRegistre32(quint16 address, int value) -{ - { - qint16 v; - v=(value&0xFFFF); - modbusDevice.setData(QModbusDataUnit::HoldingRegisters,address,v); - modbusDevice.setData(QModbusDataUnit::InputRegisters,address,v); - } - { - qint16 v; - v=((value>>16)&0xFFFF); - modbusDevice.setData(QModbusDataUnit::HoldingRegisters,address+1,v); - modbusDevice.setData(QModbusDataUnit::InputRegisters,address+1,v); - } -} - - - -void QEM24EnergyMeter::onStateChanged(int state) -{ - qDebug()<<"state :"<peerAddress() ; -} - -void QEM24EnergyMeter::setPuissanceInstantanee(int p) -{ - setRegistre32(0x12,p*10); // puissance active L1 - setRegistre32(0x28,p*10); // puissance totale - -} - -void QEM24EnergyMeter::setTension(int u) -{ - setRegistre( 0x0000, u*10 ); // V-L1 -} - -void QEM24EnergyMeter::setCourant(int i) -{ - setRegistre32( 0x000C, i*1000 ); // I-L1 -} - -void QEM24EnergyMeter::setIndexConso(long index) -{ - setRegistre32( 0x0034, index ); // - setRegistre32( 0x0040, index ); // -} - -void QEM24EnergyMeter::setIndexInjection(long index) -{ - setRegistre32( 0x004E, index ); // - -} - -void QEM24EnergyMeter::startModbus() -{ - if (modbusDevice.state()!=QModbusDevice::ConnectedState) modbusDevice.connectDevice(); -} diff --git a/qem24energymeter.h b/qem24energymeter.h deleted file mode 100644 index 2163980..0000000 --- a/qem24energymeter.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef QEM24ENERGYMETER_H -#define QEM24ENERGYMETER_H - -#include -#include -#include -#include - -class QEM24EnergyMeter : public QObject -{ - Q_OBJECT -public: - explicit QEM24EnergyMeter(QObject *parent = nullptr); - -private: - QModbusTcpServer modbusDevice; - - QModbusDataUnitMap reg; - - void setRegistre(quint16 address, quint16 value); - void setRegistre32(quint16 address, int value); - -private Q_SLOTS: - void onStateChanged(int state); - void updateWidgets(QModbusDataUnit::RegisterType table, int address, int size); - void handleDeviceError(QModbusDevice::Error newError); - - void clientDisconnected(QTcpSocket *modbusClient); - -public : - void setPuissanceInstantanee(int p); - void setTension(int u); - void setCourant(int i); - void setIndexConso(long index); - void setIndexInjection(long index); - - void startModbus(); - -signals: - -}; - -#endif // QEM24ENERGYMETER_H diff --git a/src/Config.h b/src/Config.h new file mode 100644 index 0000000..4fd9624 --- /dev/null +++ b/src/Config.h @@ -0,0 +1,43 @@ +#pragma once + +#ifdef __has_include +#if __has_include("ConfigUser.h") +#include "ConfigUser.h" +#endif +#endif + +// WiFi +#ifndef WIFI_SSID +#define WIFI_SSID "" +#endif + +#ifndef WIFI_PASSWORD +#define WIFI_PASSWORD "" +#endif + +// MQTT Broker +#ifndef MQTT_HOST +#define MQTT_HOST "192.168.0.50" +#endif + +#ifndef MQTT_PORT +#define MQTT_PORT 1883 +#endif + +#ifndef MQTT_USER +#define MQTT_USER "" +#endif + +#ifndef MQTT_PASS +#define MQTT_PASS "" +#endif + +// MQTT Topics - format EM24 pour Victron (dbus-mqtt-grid) +#ifndef MQTT_TOPIC_PREFIX +#define MQTT_TOPIC_PREFIX "linky" +#endif + +// Intervalle de publication MQTT (ms) +#ifndef MQTT_PUBLISH_INTERVAL_MS +#define MQTT_PUBLISH_INTERVAL_MS 2000 +#endif diff --git a/src/ConfigUser.example.h b/src/ConfigUser.example.h new file mode 100644 index 0000000..0fc652a --- /dev/null +++ b/src/ConfigUser.example.h @@ -0,0 +1,17 @@ +#pragma once + +// Copiez ce fichier en src/ConfigUser.h (ignore par git) et adaptez les valeurs. + +#define WIFI_SSID "MonWifi" +#define WIFI_PASSWORD "MonMotDePasse" + +// IP ou hostname du broker MQTT. +#define MQTT_HOST "192.168.0.50" +#define MQTT_PORT 1883 + +// Laisser vide si pas d'authentification. +#define MQTT_USER "" +#define MQTT_PASS "" + +#define MQTT_TOPIC_PREFIX "linky" +#define MQTT_PUBLISH_INTERVAL_MS 2000 diff --git a/src/MqttPublisher.cpp b/src/MqttPublisher.cpp new file mode 100644 index 0000000..b808976 --- /dev/null +++ b/src/MqttPublisher.cpp @@ -0,0 +1,139 @@ +#include "MqttPublisher.h" + +#include +#include +#include + +MqttPublisher::MqttPublisher(const char* fallbackHost, uint16_t fallbackPort, + const char* user, const char* pass, + const char* topicPrefix, uint32_t publishIntervalMs) + : fallbackHost_(fallbackHost), + fallbackPort_(fallbackPort), + user_(user), + pass_(pass), + publishIntervalMs_(publishIntervalMs) { + strncpy(topicPrefix_, topicPrefix, sizeof(topicPrefix_) - 1); + topicPrefix_[sizeof(topicPrefix_) - 1] = '\0'; +} + +void MqttPublisher::begin() { + mqtt_.setClient(wifiClient_); + snprintf(clientId_, sizeof(clientId_), "linky-em24-%06lX", + static_cast(ESP.getChipId())); +} + +bool MqttPublisher::discoverBroker() { + const uint32_t now = millis(); + if (now - lastDiscoveryMs_ < kDiscoveryIntervalMs) return brokerDiscovered_; + lastDiscoveryMs_ = now; + + if (!mdnsStarted_) { + if (!MDNS.begin(clientId_)) return false; + mdnsStarted_ = true; + } + + MDNS.update(); + + int n = MDNS.queryService("mqtt", "tcp"); + if (n > 0) { + brokerIP_ = MDNS.IP(0); + brokerPort_ = MDNS.port(0); + brokerDiscovered_ = true; + mqtt_.setServer(brokerIP_, brokerPort_); + return true; + } + + // Fallback sur l'hote configure (IP ou DNS) + if (!brokerDiscovered_) { + brokerPort_ = fallbackPort_; + if (brokerIP_.fromString(fallbackHost_) || WiFi.hostByName(fallbackHost_, brokerIP_)) { + mqtt_.setServer(brokerIP_, brokerPort_); + } else { + brokerIP_ = IPAddress(0, 0, 0, 0); + mqtt_.setServer(fallbackHost_, brokerPort_); + } + brokerDiscovered_ = true; + } + + return brokerDiscovered_; +} + +void MqttPublisher::update(bool wifiConnected, const TeleinfoData& data) { + if (!wifiConnected) return; + + if (!brokerDiscovered_) { + discoverBroker(); + if (!brokerDiscovered_) return; + } + + if (!mqtt_.connected()) { + const uint32_t now = millis(); + if (now - lastReconnectMs_ < kReconnectIntervalMs) return; + lastReconnectMs_ = now; + + // Re-tenter la decouverte si connexion echoue + discoverBroker(); + reconnect(); + if (!mqtt_.connected()) return; + } + + mqtt_.loop(); + + if (!data.valid) return; + + const uint32_t now = millis(); + if (now - lastPublishMs_ < publishIntervalMs_) return; + lastPublishMs_ = now; + + publishGrid(data); +} + +void MqttPublisher::reconnect() { + if (user_[0] != '\0') { + mqtt_.connect(clientId_, user_, pass_); + } else { + mqtt_.connect(clientId_); + } +} + +void MqttPublisher::publish(const char* suffix, const char* value) { + char topic[128]; + snprintf(topic, sizeof(topic), "%s/%s", topicPrefix_, suffix); + mqtt_.publish(topic, value, true); +} + +void MqttPublisher::publishInt(const char* suffix, int value) { + char buf[16]; + snprintf(buf, sizeof(buf), "%d", value); + publish(suffix, buf); +} + +void MqttPublisher::publishFloat(const char* suffix, float value) { + char buf[16]; + dtostrf(value, 1, 1, buf); + publish(suffix, buf); +} + +void MqttPublisher::publishGrid(const TeleinfoData& data) { + publishInt("Ac/Power", data.totalPowerInst); + + const char* phaseNames[] = {"Ac/L1", "Ac/L2", "Ac/L3"}; + for (size_t i = 0; i < 3; ++i) { + char suffix[32]; + + snprintf(suffix, sizeof(suffix), "%s/Voltage", phaseNames[i]); + publishInt(suffix, data.voltage[i]); + + snprintf(suffix, sizeof(suffix), "%s/Current", phaseNames[i]); + publishInt(suffix, data.current[i]); + + snprintf(suffix, sizeof(suffix), "%s/Power", phaseNames[i]); + publishInt(suffix, data.phasePowerInst[i]); + } + + const float energyForward = static_cast(data.indexHC + data.indexHP) / 10.0f; + const float energyReverse = static_cast(data.indexInj) / 10.0f; + + publishFloat("Ac/Energy/Forward", energyForward); + publishFloat("Ac/Energy/Reverse", energyReverse); +} diff --git a/src/MqttPublisher.h b/src/MqttPublisher.h new file mode 100644 index 0000000..2f3c24f --- /dev/null +++ b/src/MqttPublisher.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include + +#include "TeleinfoData.h" + +class MqttPublisher { + public: + MqttPublisher(const char* fallbackHost, uint16_t fallbackPort, + const char* user, const char* pass, + const char* topicPrefix, uint32_t publishIntervalMs); + + void begin(); + void update(bool wifiConnected, const TeleinfoData& data); + + bool isConnected() { return mqtt_.connected(); } + bool brokerDiscovered() const { return brokerDiscovered_; } + IPAddress brokerIP() const { return brokerIP_; } + uint16_t brokerPort() const { return brokerPort_; } + + private: + bool discoverBroker(); + void reconnect(); + void publish(const char* suffix, const char* value); + void publishInt(const char* suffix, int value); + void publishFloat(const char* suffix, float value); + void publishGrid(const TeleinfoData& data); + + WiFiClient wifiClient_; + PubSubClient mqtt_; + + const char* fallbackHost_; + uint16_t fallbackPort_; + const char* user_; + const char* pass_; + char topicPrefix_[64]; + char clientId_[32] = "linky-em24"; + uint32_t publishIntervalMs_; + + bool brokerDiscovered_ = false; + bool mdnsStarted_ = false; + IPAddress brokerIP_; + uint16_t brokerPort_ = 1883; + + uint32_t lastPublishMs_ = 0; + uint32_t lastReconnectMs_ = 0; + uint32_t lastDiscoveryMs_ = 0; + static constexpr uint32_t kReconnectIntervalMs = 5000; + static constexpr uint32_t kDiscoveryIntervalMs = 15000; +}; diff --git a/src/OledDisplay.cpp b/src/OledDisplay.cpp new file mode 100644 index 0000000..8358dc4 --- /dev/null +++ b/src/OledDisplay.cpp @@ -0,0 +1,192 @@ +#include "OledDisplay.h" + +#include +#include +#include + +OledDisplay::OledDisplay(uint8_t sda, uint8_t scl) + : sda_(sda), scl_(scl), display_(kWidth, kHeight, &Wire, -1) {} + +bool OledDisplay::begin() { + Wire.begin(sda_, scl_); + Wire.setClock(400000); + return display_.begin(SSD1306_SWITCHCAPVCC, kI2cAddress); +} + +void OledDisplay::showInit() { + display_.clearDisplay(); + display_.setTextSize(1); + display_.setTextColor(SSD1306_WHITE); + display_.setCursor(0, 0); + display_.println(F("Init Linky TRI")); + display_.println(F("SDA:D1 SCL:D2")); + display_.println(F("TIC RX:D7")); + display_.println(F("LibTeleinfo+MQTT")); + display_.display(); +} + +void OledDisplay::drawStatusBar(const ConnStatus& conn) { + // Derniere ligne (y=56) : barre de statut WiFi/MQTT + char bar[22]; + if (!conn.wifiConnected) { + snprintf(bar, sizeof(bar), "WiFi:-- MQTT:--"); + } else if (!conn.mqttConnected) { + snprintf(bar, sizeof(bar), "WiFi:%ddB MQTT:--", conn.rssi); + } else { + snprintf(bar, sizeof(bar), "WiFi:%ddB MQTT:OK", conn.rssi); + } + display_.setCursor(0, 56); + display_.print(bar); +} + +void OledDisplay::update(const TeleinfoData& data, TeleinfoProtocol protocol, + long baudrate, const char* modeLabel, bool autoBaud, + const ConnStatus& conn) { + const uint32_t now = millis(); + + if (now - lastPageSwitchMs_ >= kPageSwitchMs) { + lastPageSwitchMs_ = now; + currentPage_ = (currentPage_ + 1) % kPageCount; + } + + if (now - lastRefreshMs_ < kRefreshMs) return; + lastRefreshMs_ = now; + + if (!data.valid) { + drawWaiting(baudrate, modeLabel, autoBaud, conn); + return; + } + + switch (currentPage_) { + case 0: drawRealtime(data, protocol, baudrate, conn); break; + case 1: drawEnergy(data); break; + case 2: // Page statut connexion + display_.clearDisplay(); + display_.setTextSize(1); + display_.setTextColor(SSD1306_WHITE); + display_.setCursor(0, 0); + display_.println(F("Linky TRI Network")); + { + char line[40]; + snprintf(line, sizeof(line), "WiFi: %s", conn.wifiConnected ? "OK" : "..."); + display_.println(line); + if (conn.wifiConnected) { + snprintf(line, sizeof(line), "IP:%s", WiFi.localIP().toString().c_str()); + display_.println(line); + snprintf(line, sizeof(line), "RSSI: %d dBm", conn.rssi); + display_.println(line); + } else { + display_.println(F("IP: --")); + display_.println(F("RSSI: --")); + } + snprintf(line, sizeof(line), "MQTT: %s", conn.mqttConnected ? "OK" : "..."); + display_.println(line); + if (conn.mqttConnected) { + snprintf(line, sizeof(line), "Broker:%s", conn.brokerIP.toString().c_str()); + display_.println(line); + } + } + display_.display(); + break; + } +} + +void OledDisplay::drawWaiting(long baudrate, const char* modeLabel, + bool autoBaud, const ConnStatus& conn) { + display_.clearDisplay(); + display_.setTextSize(1); + display_.setTextColor(SSD1306_WHITE); + display_.setCursor(0, 0); + display_.println(F("Linky TRI OLED")); + display_.println(F("Attente trame...")); + display_.print(F("Baud: ")); + display_.println(baudrate); + display_.print(F("Mode: ")); + display_.println(modeLabel); + display_.println(autoBaud ? F("Auto 9600/1200 ON") : F("Auto 9600/1200 OFF")); + + { + char line[22]; + snprintf(line, sizeof(line), "WiFi:%s MQTT:%s", + conn.wifiConnected ? "OK" : "--", + conn.mqttConnected ? "OK" : "--"); + display_.println(line); + } + + display_.display(); +} + +void OledDisplay::drawRealtime(const TeleinfoData& data, + TeleinfoProtocol protocol, long baudrate, + const ConnStatus& conn) { + char line[40]; + + display_.clearDisplay(); + display_.setTextSize(1); + display_.setTextColor(SSD1306_WHITE); + display_.setCursor(0, 0); + + snprintf(line, sizeof(line), "PT:%6dW T:%s", data.totalPowerInst, data.tarif); + display_.println(line); + + for (size_t i = 0; i < 3; ++i) { + snprintf(line, sizeof(line), "L%u U%3d I%3d P%5d", + static_cast(i + 1), + data.voltage[i], data.current[i], data.phasePowerInst[i]); + display_.println(line); + } + + snprintf(line, sizeof(line), "B%ld %s", baudrate, protocolStr(protocol)); + display_.println(line); + + display_.println(); + drawStatusBar(conn); + + display_.display(); +} + +void OledDisplay::drawEnergy(const TeleinfoData& data) { + char hcText[24], hpText[24], injText[24], totalText[24], line[40]; + + formatTenthKwh(data.indexHC, hcText, sizeof(hcText)); + formatTenthKwh(data.indexHP, hpText, sizeof(hpText)); + formatTenthKwh(data.indexInj, injText, sizeof(injText)); + formatTenthKwh(data.indexHC + data.indexHP, totalText, sizeof(totalText)); + + display_.clearDisplay(); + display_.setTextSize(1); + display_.setTextColor(SSD1306_WHITE); + display_.setCursor(0, 0); + + display_.println(F("Linky TRI Indexes")); + + snprintf(line, sizeof(line), "HC : %s kWh", hcText); + display_.println(line); + snprintf(line, sizeof(line), "HP : %s kWh", hpText); + display_.println(line); + snprintf(line, sizeof(line), "INJ: %s kWh", injText); + display_.println(line); + snprintf(line, sizeof(line), "TOT: %s kWh", totalText); + display_.println(line); + snprintf(line, sizeof(line), "Tarif: %s", data.tarif); + display_.println(line); + + display_.display(); +} + +void OledDisplay::formatTenthKwh(int64_t value, char* out, size_t outSize) { + const bool negative = value < 0; + const uint64_t absVal = negative ? static_cast(-value) + : static_cast(value); + const unsigned long long whole = static_cast(absVal / 10ULL); + const unsigned int decimal = static_cast(absVal % 10ULL); + snprintf(out, outSize, "%s%llu.%u", negative ? "-" : "", whole, decimal); +} + +const char* OledDisplay::protocolStr(TeleinfoProtocol p) { + switch (p) { + case TeleinfoProtocol::Standard: return "STD"; + case TeleinfoProtocol::Historical: return "HIS"; + default: return "UNK"; + } +} diff --git a/src/OledDisplay.h b/src/OledDisplay.h new file mode 100644 index 0000000..bea3a87 --- /dev/null +++ b/src/OledDisplay.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +#include "TeleinfoData.h" + +struct ConnStatus { + bool wifiConnected; + bool mqttConnected; + int32_t rssi; + IPAddress brokerIP; +}; + +class OledDisplay { + public: + static constexpr uint8_t kWidth = 128; + static constexpr uint8_t kHeight = 64; + static constexpr uint8_t kI2cAddress = 0x3C; + static constexpr uint32_t kRefreshMs = 500; + static constexpr uint32_t kPageSwitchMs = 4000; + static constexpr uint8_t kPageCount = 3; + + OledDisplay(uint8_t sda, uint8_t scl); + + bool begin(); + void showInit(); + void update(const TeleinfoData& data, TeleinfoProtocol protocol, + long baudrate, const char* modeLabel, bool autoBaud, + const ConnStatus& conn); + + private: + void drawWaiting(long baudrate, const char* modeLabel, bool autoBaud, + const ConnStatus& conn); + void drawRealtime(const TeleinfoData& data, TeleinfoProtocol protocol, + long baudrate, const ConnStatus& conn); + void drawEnergy(const TeleinfoData& data); + void drawStatusBar(const ConnStatus& conn); + + static void formatTenthKwh(int64_t value, char* out, size_t outSize); + static const char* protocolStr(TeleinfoProtocol p); + + uint8_t sda_; + uint8_t scl_; + Adafruit_SSD1306 display_; + uint32_t lastRefreshMs_ = 0; + uint32_t lastPageSwitchMs_ = 0; + uint8_t currentPage_ = 0; +}; diff --git a/src/TeleinfoData.h b/src/TeleinfoData.h new file mode 100644 index 0000000..940476a --- /dev/null +++ b/src/TeleinfoData.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +enum class TeleinfoProtocol : uint8_t { + Unknown, + Standard, + Historical, +}; + +struct TeleinfoData { + int current[3] = {0, 0, 0}; + int voltage[3] = {230, 230, 230}; + + int phasePowerOut[3] = {0, 0, 0}; + int phasePowerIn[3] = {0, 0, 0}; + bool hasPowerOutPhase[3] = {false, false, false}; + bool hasPowerInPhase[3] = {false, false, false}; + + int totalPowerOut = -1; + int totalPowerIn = -1; + int totalPowerApparent = -1; + + int phasePowerInst[3] = {0, 0, 0}; + int totalPowerInst = 0; + + int64_t indexHC = 0; + int64_t indexHP = 0; + int64_t indexInj = 0; + + bool valid = false; + char tarif[16] = "--"; +}; diff --git a/src/TeleinfoReader.cpp b/src/TeleinfoReader.cpp new file mode 100644 index 0000000..06609fa --- /dev/null +++ b/src/TeleinfoReader.cpp @@ -0,0 +1,251 @@ +#include "TeleinfoReader.h" + +#include +#include +#include + +TeleinfoReader* TeleinfoReader::instance_ = nullptr; + +constexpr TeleinfoReader::ModeConfig TeleinfoReader::kModes[]; + +TeleinfoReader::TeleinfoReader(uint8_t rxPin) : rxPin_(rxPin) { + instance_ = this; +} + +void TeleinfoReader::begin() { + tinfo_.attachNewFrame(onNewFrame); + tinfo_.attachUpdatedFrame(onUpdatedFrame); + beginMode(0); +} + +void TeleinfoReader::update() { + while (serial_.available() > 0) { + tinfo_.process(static_cast(serial_.read())); + } + maybeSwitchMode(); +} + +const char* TeleinfoReader::protocolLabel() const { + switch (protocol_) { + case TeleinfoProtocol::Standard: + return "STD"; + case TeleinfoProtocol::Historical: + return "HIS"; + default: + return "UNK"; + } +} + +void TeleinfoReader::beginMode(size_t modeIndex) { + modeIndex_ = modeIndex % kModeCount; + serial_.begin(kModes[modeIndex_].baudrate, SWSERIAL_7E1, rxPin_, -1, false); + while (serial_.available() > 0) { + serial_.read(); + } + tinfo_.init(kModes[modeIndex_].mode); + + protocol_ = (kModes[modeIndex_].mode == TINFO_MODE_STANDARD) + ? TeleinfoProtocol::Standard + : TeleinfoProtocol::Historical; + + data_.valid = false; + modeStartedMs_ = millis(); + lastValidFrameMs_ = modeStartedMs_; +} + +void TeleinfoReader::maybeSwitchMode() { + if (!autoBaud_) return; + + const uint32_t now = millis(); + if ((now - modeStartedMs_) < kModeSwitchTimeoutMs) return; + if ((now - lastValidFrameMs_) < kModeSwitchTimeoutMs) return; + + beginMode((modeIndex_ + 1) % kModeCount); +} + +int TeleinfoReader::roundTo50(int value) { + if (value >= 0) return (value / 50) * 50; + return -(((-value) / 50) * 50); +} + +const char* TeleinfoReader::findLabelValue(ValueList* root, const char* label) { + if (!root || !label) return nullptr; + ValueList* node = root; + while (node->next) { + node = node->next; + if (!node->name || !node->value) continue; + if (strcmp(node->name, label) == 0) return node->value; + } + return nullptr; +} + +bool TeleinfoReader::readLabelInt(ValueList* root, const char* label, int& out) { + const char* v = findLabelValue(root, label); + if (!v || *v == '\0') return false; + out = atoi(v); + return true; +} + +bool TeleinfoReader::readLabelInt64(ValueList* root, const char* label, int64_t& out) { + const char* v = findLabelValue(root, label); + if (!v || *v == '\0') return false; + out = atoll(v); + return true; +} + +void TeleinfoReader::updateProtocol(ValueList* list) { + if (findLabelValue(list, "DATE")) { + protocol_ = TeleinfoProtocol::Standard; + return; + } + if (findLabelValue(list, "ADCO")) { + protocol_ = TeleinfoProtocol::Historical; + } +} + +void TeleinfoReader::parseFrame(ValueList* list) { + if (!list) return; + + TeleinfoData d = data_; + d.valid = true; + + const char* tarif = findLabelValue(list, "PTEC"); + if (!tarif) tarif = findLabelValue(list, "NTARF"); + if (tarif) { + strncpy(d.tarif, tarif, sizeof(d.tarif) - 1); + d.tarif[sizeof(d.tarif) - 1] = '\0'; + } + + int v = 0; + if (!readLabelInt(list, "IRMS1", v) && !readLabelInt(list, "IINST1", v)) + readLabelInt(list, "IINST", v); + d.current[0] = v; + + if (readLabelInt(list, "IRMS2", v) || readLabelInt(list, "IINST2", v)) + d.current[1] = v; + if (readLabelInt(list, "IRMS3", v) || readLabelInt(list, "IINST3", v)) + d.current[2] = v; + + if (!readLabelInt(list, "URMS1", v)) readLabelInt(list, "URMS", v); + if (v > 0) d.voltage[0] = v; + if (readLabelInt(list, "URMS2", v) && v > 0) d.voltage[1] = v; + if (readLabelInt(list, "URMS3", v) && v > 0) d.voltage[2] = v; + + d.totalPowerOut = -1; + d.totalPowerIn = -1; + d.totalPowerApparent = -1; + for (size_t i = 0; i < 3; ++i) { + d.hasPowerOutPhase[i] = false; + d.hasPowerInPhase[i] = false; + } + + if (readLabelInt(list, "SINSTS", v)) d.totalPowerOut = v; + if (readLabelInt(list, "SINSTI", v)) d.totalPowerIn = v; + if (readLabelInt(list, "PAPP", v)) d.totalPowerApparent = v; + + const char* outLabels[] = {"SINSTS1", "SINSTS2", "SINSTS3"}; + const char* inLabels[] = {"SINSTI1", "SINSTI2", "SINSTI3"}; + for (size_t i = 0; i < 3; ++i) { + if (readLabelInt(list, outLabels[i], v)) { + d.phasePowerOut[i] = v; + d.hasPowerOutPhase[i] = true; + } + if (readLabelInt(list, inLabels[i], v)) { + d.phasePowerIn[i] = v; + d.hasPowerInPhase[i] = true; + } + } + + int64_t lv = 0; + bool baseRead = false; + if (readLabelInt64(list, "BASE", lv)) { + d.indexHC = lv / 100; + d.indexHP = 0; + baseRead = true; + } + if (readLabelInt64(list, "EASF01", lv) || readLabelInt64(list, "HCHC", lv)) + d.indexHC = lv / 100; + if (!baseRead && (readLabelInt64(list, "EASF02", lv) || readLabelInt64(list, "HCHP", lv))) + d.indexHP = lv / 100; + if (readLabelInt64(list, "EAIT", lv)) + d.indexInj = lv / 100; + + recomputeDerived(d); + updateProtocol(list); + + data_ = d; + lastValidFrameMs_ = millis(); +} + +void TeleinfoReader::recomputeDerived(TeleinfoData& d) { + int sumOutPhase = 0, sumInPhase = 0; + bool hasOutPhase = false, hasInPhase = false; + + for (size_t i = 0; i < 3; ++i) { + if (d.hasPowerOutPhase[i]) { hasOutPhase = true; sumOutPhase += d.phasePowerOut[i]; } + if (d.hasPowerInPhase[i]) { hasInPhase = true; sumInPhase += d.phasePowerIn[i]; } + } + + int totalOut = d.totalPowerOut >= 0 ? d.totalPowerOut : (hasOutPhase ? sumOutPhase : -1); + int totalIn = d.totalPowerIn >= 0 ? d.totalPowerIn : (hasInPhase ? sumInPhase : -1); + if (totalOut < 0 && d.totalPowerApparent >= 0) totalOut = d.totalPowerApparent; + + const bool hasInjection = totalIn > 0; + + if (hasInjection) + d.totalPowerInst = -roundTo50(totalIn); + else if (totalOut >= 0) + d.totalPowerInst = roundTo50(totalOut); + else + d.totalPowerInst = 0; + + bool hasInstantPhasePower = false; + for (size_t i = 0; i < 3; ++i) { + d.phasePowerInst[i] = 0; + const int phaseOut = d.hasPowerOutPhase[i] ? d.phasePowerOut[i] : 0; + const int phaseIn = d.hasPowerInPhase[i] ? d.phasePowerIn[i] : 0; + if (hasInjection && phaseIn > 0) { + d.phasePowerInst[i] = -roundTo50(phaseIn); + hasInstantPhasePower = true; + } else if (!hasInjection && phaseOut > 0) { + d.phasePowerInst[i] = roundTo50(phaseOut); + hasInstantPhasePower = true; + } + } + + if (!hasInstantPhasePower && d.totalPowerInst != 0) { + int sumCurrentAbs = 0; + int currentAbs[3] = {0, 0, 0}; + for (size_t i = 0; i < 3; ++i) { + currentAbs[i] = abs(d.current[i]); + sumCurrentAbs += currentAbs[i]; + } + if (sumCurrentAbs > 0) { + int assigned = 0; + for (size_t i = 0; i < 3; ++i) { + if (i < 2) { + d.phasePowerInst[i] = (d.totalPowerInst * currentAbs[i]) / sumCurrentAbs; + assigned += d.phasePowerInst[i]; + } else { + d.phasePowerInst[i] = d.totalPowerInst - assigned; + } + } + } + } + + for (size_t i = 0; i < 3; ++i) { + if (hasInjection) { + if (d.current[i] > 0) d.current[i] = -d.current[i]; + } else { + if (d.current[i] < 0) d.current[i] = -d.current[i]; + } + } +} + +void TeleinfoReader::onNewFrame(ValueList* list) { + if (instance_) instance_->parseFrame(list); +} + +void TeleinfoReader::onUpdatedFrame(ValueList* list) { + if (instance_) instance_->parseFrame(list); +} diff --git a/src/TeleinfoReader.h b/src/TeleinfoReader.h new file mode 100644 index 0000000..c984e29 --- /dev/null +++ b/src/TeleinfoReader.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +#include "TeleinfoData.h" + +class TeleinfoReader { + public: + static constexpr uint32_t kModeSwitchTimeoutMs = 8000; + + TeleinfoReader(uint8_t rxPin); + + void begin(); + void update(); + + const TeleinfoData& data() const { return data_; } + TeleinfoProtocol protocol() const { return protocol_; } + long baudrate() const { return kModes[modeIndex_].baudrate; } + const char* modeLabel() const { return kModes[modeIndex_].label; } + const char* protocolLabel() const; + bool autoBaud() const { return autoBaud_; } + + private: + struct ModeConfig { + long baudrate; + _Mode_e mode; + const char* label; + }; + + static constexpr ModeConfig kModes[] = { + {9600, TINFO_MODE_STANDARD, "STD"}, + {1200, TINFO_MODE_HISTORIQUE, "HIS"}, + }; + static constexpr size_t kModeCount = sizeof(kModes) / sizeof(kModes[0]); + + static int roundTo50(int value); + static const char* findLabelValue(ValueList* root, const char* label); + static bool readLabelInt(ValueList* root, const char* label, int& out); + static bool readLabelInt64(ValueList* root, const char* label, int64_t& out); + + void beginMode(size_t modeIndex); + void maybeSwitchMode(); + void parseFrame(ValueList* list); + void recomputeDerived(TeleinfoData& d); + void updateProtocol(ValueList* list); + + static void onNewFrame(ValueList* list); + static void onUpdatedFrame(ValueList* list); + + uint8_t rxPin_; + bool autoBaud_ = true; + SoftwareSerial serial_; + TInfo tinfo_; + TeleinfoData data_; + TeleinfoProtocol protocol_ = TeleinfoProtocol::Unknown; + size_t modeIndex_ = 0; + uint32_t modeStartedMs_ = 0; + uint32_t lastValidFrameMs_ = 0; + + static TeleinfoReader* instance_; +}; diff --git a/src/WifiManager.cpp b/src/WifiManager.cpp new file mode 100644 index 0000000..6cb0ce0 --- /dev/null +++ b/src/WifiManager.cpp @@ -0,0 +1,27 @@ +#include "WifiManager.h" + +#include + +WifiManager::WifiManager(const char* ssid, const char* password) + : ssid_(ssid), password_(password) {} + +void WifiManager::begin() { + WiFi.mode(WIFI_STA); + WiFi.persistent(false); + WiFi.setAutoReconnect(true); + connect(); +} + +void WifiManager::update() { + if (isConnected()) return; + + const uint32_t now = millis(); + if (now - lastAttemptMs_ < kReconnectIntervalMs) return; + + connect(); +} + +void WifiManager::connect() { + lastAttemptMs_ = millis(); + WiFi.begin(ssid_, password_); +} diff --git a/src/WifiManager.h b/src/WifiManager.h new file mode 100644 index 0000000..73e1660 --- /dev/null +++ b/src/WifiManager.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +class WifiManager { + public: + WifiManager(const char* ssid, const char* password); + + void begin(); + void update(); + + bool isConnected() const { return WiFi.status() == WL_CONNECTED; } + IPAddress localIP() const { return WiFi.localIP(); } + int32_t rssi() const { return WiFi.RSSI(); } + + private: + void connect(); + + const char* ssid_; + const char* password_; + uint32_t lastAttemptMs_ = 0; + static constexpr uint32_t kReconnectIntervalMs = 10000; +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..824a971 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,55 @@ +#include + +#include "Config.h" +#include "MqttPublisher.h" +#include "OledDisplay.h" +#include "TeleinfoReader.h" +#include "WifiManager.h" + +namespace { +constexpr uint8_t PIN_SDA = D1; +constexpr uint8_t PIN_SCL = D2; +constexpr uint8_t PIN_TELEINFO_RX = D7; + +OledDisplay oled(PIN_SDA, PIN_SCL); +TeleinfoReader teleinfo(PIN_TELEINFO_RX); +WifiManager wifi(WIFI_SSID, WIFI_PASSWORD); +MqttPublisher mqtt(MQTT_HOST, MQTT_PORT, MQTT_USER, MQTT_PASS, + MQTT_TOPIC_PREFIX, MQTT_PUBLISH_INTERVAL_MS); +} // namespace + +void setup() { + Serial.begin(9600); + Serial.println(F("\n[Linky TRI] Boot")); + + if (!oled.begin()) { + Serial.println(F("[OLED] FAIL")); + while (true) delay(1000); + } + oled.showInit(); + + teleinfo.begin(); + Serial.println(F("[TIC] OK")); + + wifi.begin(); + Serial.println(F("[WiFi] Connecting...")); + + mqtt.begin(); + Serial.println(F("[MQTT] Ready")); +} + +void loop() { + teleinfo.update(); + wifi.update(); + mqtt.update(wifi.isConnected(), teleinfo.data()); + + ConnStatus conn; + conn.wifiConnected = wifi.isConnected(); + conn.mqttConnected = mqtt.isConnected(); + conn.rssi = wifi.rssi(); + conn.brokerIP = mqtt.brokerIP(); + + oled.update(teleinfo.data(), teleinfo.protocol(), + teleinfo.baudrate(), teleinfo.modeLabel(), + teleinfo.autoBaud(), conn); +} diff --git a/teleinfoMqtt.pro b/teleinfoMqtt.pro deleted file mode 100644 index a645c06..0000000 --- a/teleinfoMqtt.pro +++ /dev/null @@ -1,34 +0,0 @@ -QT -= gui -QT += mqtt xml serialport serialbus - -CONFIG += c++11 console -CONFIG -= app_bundle - -# The following define makes your compiler emit warnings if you use -# any Qt feature that has been marked deprecated (the exact warnings -# depend on your compiler). Please consult the documentation of the -# deprecated API in order to know how to port your code away from it. -#DEFINES += QT_DEPRECATED_WARNINGS - -# You can also make your code fail to compile if it uses deprecated APIs. -# In order to do so, uncomment the following line. -# You can also select to disable deprecated APIs only up to a certain version of Qt. -#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 - -SOURCES += \ - main.cpp \ - compteur.cpp \ - qem24energymeter.cpp - -# Default rules for deployment. -target.path = /home/bas -INSTALLS += target - -HEADERS += \ - compteur.h \ - qem24energymeter.h - -#LIBS += "/opt/electronique/navis/sysroot/lib/aarch64-linux-gnu/libudev.so.1" - -DISTFILES += \ - teleinfoData.txt diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..9111588 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,12 @@ +# Smoke Tests + +## Objectif +Valider rapidement la compilabilité et la cohérence de configuration. + +## Commandes recommandées +- `pio run` +- `cmake -S . -B build && cmake --build build` (si CMake est présent) + +## Critères de réussite +- Build sans erreur bloquante. +- Configuration cible cohérente.