From d6aacd7a8781c9aa4a53dfa81f5ec97b73c52687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:12:22 +0100 Subject: [PATCH] chore(firmware): stage local changes Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 33 ++++ .gitignore | 6 + .vscode/extensions.json | 10 ++ .vscode/settings.json | 4 + LICENSE.md | 3 + README.md | 114 ++++++++++++- compteur.cpp | 336 --------------------------------------- compteur.h | 81 ---------- main.cpp | 9 -- platformio.ini | 10 ++ qem24energymeter.cpp | 216 ------------------------- qem24energymeter.h | 43 ----- src/Config.h | 43 +++++ src/ConfigUser.example.h | 17 ++ src/MqttPublisher.cpp | 139 ++++++++++++++++ src/MqttPublisher.h | 52 ++++++ src/OledDisplay.cpp | 192 ++++++++++++++++++++++ src/OledDisplay.h | 49 ++++++ src/TeleinfoData.h | 33 ++++ src/TeleinfoReader.cpp | 251 +++++++++++++++++++++++++++++ src/TeleinfoReader.h | 62 ++++++++ src/WifiManager.cpp | 27 ++++ src/WifiManager.h | 23 +++ src/main.cpp | 55 +++++++ teleinfoMqtt.pro | 34 ---- tests/README.md | 12 ++ 26 files changed, 1134 insertions(+), 720 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 LICENSE.md delete mode 100644 compteur.cpp delete mode 100644 compteur.h delete mode 100644 main.cpp create mode 100644 platformio.ini delete mode 100644 qem24energymeter.cpp delete mode 100644 qem24energymeter.h create mode 100644 src/Config.h create mode 100644 src/ConfigUser.example.h create mode 100644 src/MqttPublisher.cpp create mode 100644 src/MqttPublisher.h create mode 100644 src/OledDisplay.cpp create mode 100644 src/OledDisplay.h create mode 100644 src/TeleinfoData.h create mode 100644 src/TeleinfoReader.cpp create mode 100644 src/TeleinfoReader.h create mode 100644 src/WifiManager.cpp create mode 100644 src/WifiManager.h create mode 100644 src/main.cpp delete mode 100644 teleinfoMqtt.pro create mode 100644 tests/README.md 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.