chore(firmware): stage local changes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.pio
|
||||||
|
.vscode/.browse.c_cpp.db*
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/ipch
|
||||||
|
src/ConfigUser.h
|
||||||
Vendored
+10
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"stm32-for-vscode.openOCDPath": false,
|
||||||
|
"stm32-for-vscode.armToolchainPath": false
|
||||||
|
}
|
||||||
@@ -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/.
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- CHANTIER:AUDIT START -->
|
||||||
|
## 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`
|
||||||
|
|
||||||
|
<!-- CHANTIER:AUDIT END -->
|
||||||
|
|||||||
-336
@@ -1,336 +0,0 @@
|
|||||||
#include "compteur.h"
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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()<<liste<<" "<<crc;
|
|
||||||
|
|
||||||
if (crc==liste.last().at(0))
|
|
||||||
{
|
|
||||||
//qDebug()<<"crc ok";
|
|
||||||
|
|
||||||
if (liste.at(0)=="DATE")
|
|
||||||
{
|
|
||||||
//qDebug()<<"start";
|
|
||||||
if (start==false) start=true;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (injection==true)
|
|
||||||
{
|
|
||||||
if (iInst>0) iInst=-iInst;
|
|
||||||
pInst=-pInj;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (iInst<0) iInst=-iInst;
|
|
||||||
pInst=pSout;
|
|
||||||
}
|
|
||||||
if (iInst!=iInstPrec)
|
|
||||||
{
|
|
||||||
iInstPrec=iInst;
|
|
||||||
//qDebug()<<"topic"<<"fluide/elec/iinst"<<iInst;
|
|
||||||
//mqttSend("fluide/elec/iinst",QString::number(iInst),1,true);
|
|
||||||
energyMeter.setCourant(iInst);
|
|
||||||
}
|
|
||||||
else if (pInst!=pInstPrec)
|
|
||||||
{
|
|
||||||
pInstPrec=pInst;
|
|
||||||
//qDebug()<<"topic"<<"fluide/elec/puissance"<<pInst;
|
|
||||||
mqttSend("homeassistant/sensor/teleinfoPinst/state",QString::number(pInst),1,true);
|
|
||||||
energyMeter.setPuissanceInstantanee(pInst);
|
|
||||||
}
|
|
||||||
pConso=pInst+pSolaire;
|
|
||||||
if (pConso!=pConsoPrec)
|
|
||||||
{
|
|
||||||
pConsoPrec=pConso;
|
|
||||||
mqttSend("homeassistant/sensor/teleinfoPconso/state",QString::number(pConso),1,true);
|
|
||||||
}
|
|
||||||
if (indexInj!=indexInjPrec)
|
|
||||||
{
|
|
||||||
indexInjPrec=indexInj;
|
|
||||||
mqttSend("homeassistant/sensor/teleinfoIndexInjection/state",QString::number(indexInj/10)+'.'+QString::number(indexInj%10),1,true);
|
|
||||||
energyMeter.setIndexInjection(indexInj);
|
|
||||||
}
|
|
||||||
if (indexHC!=indexHCprec)
|
|
||||||
{
|
|
||||||
indexHCprec=indexHC;
|
|
||||||
indexTotal=indexHC+indexHP;
|
|
||||||
mqttSend("homeassistant/sensor/teleinfoIndexHC/state",QString::number(indexHC/10)+'.'+QString::number(indexHC%10),1,true);
|
|
||||||
}
|
|
||||||
if (indexHP!=indexHPprec)
|
|
||||||
{
|
|
||||||
indexHPprec=indexHP;
|
|
||||||
indexTotal=indexHC+indexHP;
|
|
||||||
mqttSend("homeassistant/sensor/teleinfoIndexHP/state",QString::number(indexHP/10)+'.'+QString::number(indexHP%10),1,true);
|
|
||||||
}
|
|
||||||
if (indexTotal!=indexTotalprec)
|
|
||||||
{
|
|
||||||
indexTotalprec=indexTotal;
|
|
||||||
energyMeter.setIndexConso(indexTotal);
|
|
||||||
|
|
||||||
}
|
|
||||||
if (uInst!=uInstPrec)
|
|
||||||
{
|
|
||||||
uInstPrec=uInst;
|
|
||||||
energyMeter.setTension(uInst);
|
|
||||||
}
|
|
||||||
|
|
||||||
for(int i=0;i<nbEtiquettes;i++)
|
|
||||||
{
|
|
||||||
if (listeValeur.at(i)!=listeNewValeur.at(i))
|
|
||||||
{
|
|
||||||
listeValeur[i]=listeNewValeur.at(i);
|
|
||||||
//qDebug()<<"topic"<<listeTopic.at(i)<<listeValeur[i];
|
|
||||||
mqttSend(listeTopic[i],listeValeur[i],1,true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isEnergyMeterStarted==false)
|
|
||||||
{
|
|
||||||
energyMeter.startModbus();
|
|
||||||
isEnergyMeterStarted=true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (liste.at(0)=="IRMS1")
|
|
||||||
{
|
|
||||||
iInst=liste.at(1).toInt();
|
|
||||||
}
|
|
||||||
else if (liste.at(0)=="URMS1")
|
|
||||||
{
|
|
||||||
uInst=liste.at(1).toInt();
|
|
||||||
}
|
|
||||||
else if (liste.at(0)=="SINSTS")
|
|
||||||
{
|
|
||||||
pSout=liste.at(1).toInt();
|
|
||||||
//if (pInst==0) injection=true; else injection=false;
|
|
||||||
pSout=(pSout/50)*50;
|
|
||||||
}
|
|
||||||
else if (liste.at(0)=="SINSTI")
|
|
||||||
{
|
|
||||||
pInj=liste.at(1).toInt();
|
|
||||||
if (pInj!=0) injection=true; else injection=false;
|
|
||||||
pInj=(pInj/50)*50;
|
|
||||||
}
|
|
||||||
else if (liste.at(0)=="EAIT")
|
|
||||||
{
|
|
||||||
indexInj=liste.at(1).toLong()/100;
|
|
||||||
}
|
|
||||||
else if (liste.at(0)=="EASF01")
|
|
||||||
{
|
|
||||||
indexHC = liste.at(1).toLong()/100;
|
|
||||||
}
|
|
||||||
else if (liste.at(0)=="EASF02")
|
|
||||||
{
|
|
||||||
indexHP = liste.at(1).toLong()/100;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
int i=listeEtiquette.indexOf(liste.at(0));
|
|
||||||
if (i>=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 : "<<message.payload();
|
|
||||||
bool isInt;
|
|
||||||
int val=message.payload().toInt(&isInt);
|
|
||||||
if (isInt)
|
|
||||||
{
|
|
||||||
//qDebug()<<val;
|
|
||||||
pSolaire=val;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
-81
@@ -1,81 +0,0 @@
|
|||||||
#ifndef COMPTEUR_H
|
|
||||||
#define COMPTEUR_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QSerialPort>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QtMqtt/QMqttClient>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
#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<int> listeDiv;
|
|
||||||
QList<int> 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
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#include <QCoreApplication>
|
|
||||||
#include "compteur.h"
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
|
||||||
{
|
|
||||||
QCoreApplication a(argc, argv);
|
|
||||||
Compteur cpt(&a);
|
|
||||||
return a.exec();
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
#include "qem24energymeter.h"
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
|
|
||||||
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 :"<<state;
|
|
||||||
}
|
|
||||||
|
|
||||||
void QEM24EnergyMeter::updateWidgets(QModbusDataUnit::RegisterType table, int address, int size)
|
|
||||||
{
|
|
||||||
quint16 value;
|
|
||||||
modbusDevice.data(QModbusDataUnit::HoldingRegisters,address,&value);
|
|
||||||
qDebug()<<"updateWidgets :"<<address<<" "<<size<<" "<<value;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void QEM24EnergyMeter::handleDeviceError(QModbusDevice::Error newError)
|
|
||||||
{
|
|
||||||
qDebug()<<"error :"<<newError;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void QEM24EnergyMeter::clientDisconnected(QTcpSocket *modbusClient)
|
|
||||||
{
|
|
||||||
qDebug()<<"deconnexion : "<<modbusClient->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();
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#ifndef QEM24ENERGYMETER_H
|
|
||||||
#define QEM24ENERGYMETER_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QModbusTcpServer>
|
|
||||||
#include <QTcpSocket>
|
|
||||||
#include <QHostAddress>
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
#include "MqttPublisher.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
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<unsigned long>(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<float>(data.indexHC + data.indexHP) / 10.0f;
|
||||||
|
const float energyReverse = static_cast<float>(data.indexInj) / 10.0f;
|
||||||
|
|
||||||
|
publishFloat("Ac/Energy/Forward", energyForward);
|
||||||
|
publishFloat("Ac/Energy/Reverse", energyReverse);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESP8266mDNS.h>
|
||||||
|
#include <PubSubClient.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
#include "OledDisplay.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
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<unsigned int>(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<uint64_t>(-value)
|
||||||
|
: static_cast<uint64_t>(value);
|
||||||
|
const unsigned long long whole = static_cast<unsigned long long>(absVal / 10ULL);
|
||||||
|
const unsigned int decimal = static_cast<unsigned int>(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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Adafruit_SSD1306.h>
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
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] = "--";
|
||||||
|
};
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
#include "TeleinfoReader.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
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<char>(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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <LibTeleinfo.h>
|
||||||
|
#include <SoftwareSerial.h>
|
||||||
|
|
||||||
|
#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_;
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#include "WifiManager.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
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_);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#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);
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user