Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions JS_Brute.h
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,10 @@ async function LoadData() {
GID('infoPmqtt').style.display="block";
GH('dataPmqtt', groupes[1]);
break;
case "WesV2":
GID('infoWesV2').style.display="block";
GH('dataWesV2', groupes[1]);
break;

case "Linky":
GID('infoLinky').style.display = "block";
Expand Down
16 changes: 14 additions & 2 deletions JS_Para.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ function SetParaFixe() {
GID("EnphaseUser").value = F.EnphaseUser;
GID("EnphasePwd").value = F.EnphasePwd;
GID("EnphaseSerial").value = F.EnphaseSerial;
GID("WesUser").value = F.WesUser;
GID("WesPwd").value = F.WesPwd;
GID("WesPinceNum").value = F.WesPinceNum;
GID("TopicP").value = F.TopicP;
GID("MQTTRepet").value = F.MQTTRepet;
GID("MQTTIP").value = int2ip(F.MQTTIP);
Expand Down Expand Up @@ -201,6 +204,9 @@ function SendValues() {
F.EnphaseUser = GID("EnphaseUser").value ;
F.EnphasePwd = GID("EnphasePwd").value ;
F.EnphaseSerial = GID("EnphaseSerial").value ;
F.WesUser = GID("WesUser").value ;
F.WesPwd = GID("WesPwd").value ;
F.WesPinceNum = GID("WesPinceNum").value ;

F.nomRouteur =GID("nomRouteur").value.trim() ;
F.nomSondeFixe = GID("nomSondeFixe").value.trim();
Expand Down Expand Up @@ -483,20 +489,26 @@ function AdaptationSource() {
<div class='shem'><Strong>Shelly Em Gen3</strong><br>
Courant maison sur voie 0 = 30, voie 1 = 31</div>`;
break;
case 'WesV2':
txtExt = "WES v2 Cartelectronic";
break;
}

// Mise à jour des libellés
GH('labExtIp', txtExt);
GH('label_enphase_shelly', lab_enphaseShelly);

// Visibilité de la ligne d'IP externe/Référence
const isExternalSource = ['Ext', 'Enphase', 'SmartG', 'HomeW', 'ShellyEm', 'ShellyPro'].includes(F.Source);
const isExternalSource = ['Ext', 'Enphase', 'SmartG', 'HomeW', 'ShellyEm', 'ShellyPro', 'WesV2'].includes(F.Source);
GID('ligneExt').style.display = isExternalSource ? "table-row" : "none";

// Visibilité des options d'authentification/série Enphase/Shelly
// Visibilité des options d'authentification/série Enphase/Shelly/WES
GID('ligneEnphaseUser').style.display = (F.Source === 'Enphase') ? "table-row" : "none";
GID('ligneEnphasePwd').style.display = (F.Source === 'Enphase') ? "table-row" : "none";
GID('ligneEnphaseSerial').style.display = (F.Source === 'Enphase' || F.Source === 'ShellyEm' || F.Source === 'ShellyPro') ? "table-row" : "none";
GID('ligneWesUser').style.display = (F.Source === 'WesV2') ? "table-row" : "none";
GID('ligneWesPwd').style.display = (F.Source === 'WesV2') ? "table-row" : "none";
GID('ligneWesPinceNum').style.display = (F.Source === 'WesV2') ? "table-row" : "none";
}

/**
Expand Down
7 changes: 7 additions & 0 deletions PageBrute.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const char *PageBrute = R"====(
#infoSmartG,
#infoHomeW,
#infoShellyEm,
#infoWesV2,
#infoPmqtt {
display: none;
}
Expand Down Expand Up @@ -168,6 +169,12 @@ const char *PageBrute = R"====(
<div id="dataPmqtt" class="tableau dataIn"></div>
</div>

<!-- WES v2 -->
<div id="infoWesV2">
<div>Données WES v2 Cartelectronic</div>
<div id="dataWesV2" class="tableau dataIn"></div>
</div>

<!-- Linky -->
<div id="infoLinky">
<div id="dateLinky"></div>
Expand Down
23 changes: 23 additions & 0 deletions PagePara.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const char *ParaHtml = R"====(
.Bgeneraux { border: inset 4px azure; }
#BoutonsBas { text-align:center; }
#ligneFixe, .ligneTemperature, #ligneExt, #ligneEnphaseUser, #ligneEnphasePwd, #ligneEnphaseSerial,
#ligneWesUser, #ligneWesPwd, #ligneWesPinceNum,
#infoIP, #ligneTopicP, #ligneTopicT { display:none; }
.Zone, .generaux { width:100%; border:1px solid grey; border-radius:10px; margin-top:10px;
background-color:rgba(30,30,30,0.3); }
Expand Down Expand Up @@ -347,6 +348,7 @@ const char *ParaHtml = R"====(
<option value="HomeW">HomeWizard</option>
<option value="ShellyEm">Shelly Em</option>
<option value="ShellyPro">Shelly Pro Em</option>
<option value="WesV2">WES v2 Cartelectronic</option>
<option value="Ext">ESP Externe</option>
<option value="Pmqtt">MQTT</option>
</select>
Expand Down Expand Up @@ -381,6 +383,27 @@ const char *ParaHtml = R"====(
onchange="checkDisabled();" autocomplete="on">
</div>

<div class="ligne" id="ligneWesUser">
<label for="WesUser">WES v2 User :
<span class="fsize10"><br>Nom d'utilisateur WES (défaut: admin)</span>
</label>
<input type="text" id="WesUser" name="WesUser" autocomplete="on">
</div>

<div class="ligne" id="ligneWesPwd">
<label for="WesPwd">WES v2 Password :
<span class="fsize10"><br>Mot de passe WES</span>
</label>
<input type="password" id="WesPwd" name="WesPwd" autocomplete="on">
</div>

<div class="ligne" id="ligneWesPinceNum">
<label for="WesPinceNum">WES v2 Numéro de pince :
<span class="fsize10"><br>Pince 1 à 8</span>
</label>
<input type="number" id="WesPinceNum" name="WesPinceNum" min="1" max="8" autocomplete="on">
</div>

<div class="ligne" id="ligneTopicP">
<label for="TopicP">MQTT Topic Puissance :</label>
<input type="text" id="TopicP" name="TopicP" autocomplete="on">
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Routeur photovoltaïque basé sur **ESP32**, permettant d’optimiser l’autoco
- ⚙️ **Mesures de puissance multi-sources**
- Lecture directe du **compteur Linky** via **prise TIC**.
- Mesure par méthode **UxI**, **UxIx2**, ou **UxIx3** à l’aide de sondes de courant.
- Support des capteurs externes via **MQTT**, **Shelly EM**, etc.
- Support des capteurs externes via **MQTT**, **Shelly EM**, **Enphase Envoy**, **WES v2 Cartelectronic**, etc.

- 🔌 **Pilotage des charges**
- Sorties pour **relais statiques (SSR)**.
Expand Down
3 changes: 3 additions & 0 deletions Server.ino
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ void handleAjaxRMS() { // Envoi des dernières données brutes reçues du RMS
if (Source_data == "UxIx3") {
S += GS + MK333_dataBrute;
}
if (Source_data == "WesV2") {
S += GS + Wes_dataBrute;
}
if (Source_data == "Pmqtt") {
S += GS + P_MQTT_Brute;
}
Expand Down
11 changes: 11 additions & 0 deletions Solar_Router_V17_06.ino
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,12 @@ float PwMQTT = 0;
float PvaMQTT = 0;
float PfMQTT = 1;

//Paramètres for WES v2
String Wes_dataBrute = "";
String WesUser = "admin";
String WesPwd = "";
byte WesPinceNum = 1;

//Paramètres pour RTE
byte TempoRTEon = 0;
int LastHeureRTE = -1;
Expand Down Expand Up @@ -1383,6 +1389,11 @@ void Task_LectureRMS(void *pvParameters) {
LastRMS_Millis = millis();
PeriodeProgMillis = 300 + ralenti; //On adapte la vitesse pour ne pas surchargé Wifi
}
if (Source == "WesV2") {
LectureWesV2();
LastRMS_Millis = millis();
PeriodeProgMillis = 300 + ralenti; //On adapte la vitesse pour ne pas surcharger Wifi
}

if (Source == "Ext") {
CallESP32_Externe();
Expand Down
215 changes: 215 additions & 0 deletions Source_WesV2.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// ****************************************************
// * Client WES v2 Cartelectronic - Pinces Ampèremétriques *
// ****************************************************

// Constantes pour les timeouts
const unsigned long WES_CONNECT_TIMEOUT = 3000; // Timeout de connexion en ms
const unsigned long WES_READ_TIMEOUT = 5000; // Timeout de lecture en ms

// Fonction pour extraire une valeur XML simple
// Exemple: ValXml("I1", xmlData) retourne "2.96" depuis "<I1>2.96</I1>"
float ValXml(String nom, String Xml) {
String baliseDebut = "<" + nom + ">";
String baliseFin = "</" + nom + ">";
int p1 = Xml.indexOf(baliseDebut);
if (p1 < 0)
return 0;

p1 += baliseDebut.length();
int p2 = Xml.indexOf(baliseFin, p1);
if (p2 < 0)
return 0;

String valeur = Xml.substring(p1, p2);
valeur.trim();
return valeur.toFloat();
}

// Fonction pour encoder en Base64 (pour HTTP Basic Auth)
String base64Encode(String str) {
const char *base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
String encoded = "";
int val = 0;
int valb = -6;

for (unsigned char c : str) {
val = (val << 8) + c;
valb += 8;
while (valb >= 0) {
encoded += base64_chars[(val >> valb) & 0x3F];
valb -= 6;
}
}

if (valb > -6) {
encoded += base64_chars[((val << 8) >> (valb + 8)) & 0x3F];
}

while (encoded.length() % 4) {
encoded += '=';
}

return encoded;
}

void LectureWesV2() {
String Wes_Data = "";
float current = 0; // Intensité en Ampères
float voltage = 0; // Tension en Volts
float cosPhi = 0; // Facteur de puissance
float energyIndex = 0; // Index énergie totale en kWh
float energyInject = 0; // Index énergie injectée en kWh

// Connexion HTTP au WES v2
WiFiClient clientESP_RMS;
String host = IP2String(RMSextIP);

if (!clientESP_RMS.connect(host.c_str(), 80, WES_CONNECT_TIMEOUT)) {
delay(500);
if (!clientESP_RMS.connect(host.c_str(), 80, WES_CONNECT_TIMEOUT)) {
delay(100);
clientESP_RMS.stop();
StockMessage("WES v2: Connection failed to " + host + " - Check IP and network");
return;
}
}

// Préparation de l'authentification HTTP Basic
String auth = WesUser + ":" + WesPwd;
String authEncoded = base64Encode(auth);

// Requête HTTP GET vers data.cgx
String url = "/data.cgx";
clientESP_RMS.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host +
"\r\n" + "Authorization: Basic " + authEncoded + "\r\n" +
"Connection: close\r\n\r\n");

unsigned long timeout = millis();
while (clientESP_RMS.available() == 0) {
if (millis() - timeout > WES_READ_TIMEOUT) {
StockMessage("WES v2: Timeout waiting for response from " + host);
clientESP_RMS.stop();
delay(100);
return;
}
}

// Lecture de la première ligne (status HTTP)
timeout = millis();
String statusLine = clientESP_RMS.readStringUntil('\n');

// Validation du code de statut HTTP
if (statusLine.indexOf("200") < 0) {
if (statusLine.indexOf("401") >= 0) {
StockMessage("WES v2: Authentication failed - Check username and password");
} else if (statusLine.indexOf("404") >= 0) {
StockMessage("WES v2: File not found (404) - Check WES configuration");
} else {
StockMessage("WES v2: HTTP error - " + statusLine);
}
clientESP_RMS.stop();
return;
}

// Lecture de la réponse HTTP
timeout = millis();
bool headersEnded = false;
while (clientESP_RMS.available() && (millis() - timeout < WES_READ_TIMEOUT)) {
String line = clientESP_RMS.readStringUntil('\n');

// Détection de la fin des headers HTTP (ligne vide)
if (!headersEnded) {
if (line == "\r" || line == "") {
headersEnded = true;
}
continue;
}

// Lecture du corps XML
Wes_Data += line;
}
clientESP_RMS.stop();

// Validation du numéro de pince
byte pinceNum = WesPinceNum;
if (pinceNum < 1 || pinceNum > 8) pinceNum = 1; // Validation: pince 1-8

// Stockage des données brutes pour debug
Wes_dataBrute = "<strong>WES v2 - Pince " + String(pinceNum) +
"</strong><br>" + Wes_Data;

// Extraction des données de la pince sélectionnée
String pinceTag = String(pinceNum);

// On ne garde que la partie <pince>...</pince> pour éviter de lire les <INDEXn> de la partie <impulsion>
int pStart = Wes_Data.indexOf("<pince>");
if (pStart > 0) {
Wes_Data = Wes_Data.substring(pStart);
}

// Lecture de la tension (commune à toutes les pinces)
voltage = ValXml("V", Wes_Data);

// Validation de la tension pour éviter les trous de valeurs (0V si parsing échoué)
if (voltage < 90.0) {
return;
}

// Lecture des données spécifiques à la pince
current = ValXml("I" + pinceTag, Wes_Data);
cosPhi = ValXml("COSPHI" + pinceTag, Wes_Data);
energyIndex = ValXml("INDEX" + pinceTag, Wes_Data);
energyInject = ValXml("IDXINJECT" + pinceTag, Wes_Data);

// Calcul de la puissance active : P = V * I * |cos(phi)|
// Le signe de COSPHI indique le sens :
// - COSPHI positif = consommation (soutirage)
// - COSPHI négatif = production (injection)
float Pva = voltage * current; // Puissance apparente S = V * I
float PwAbs = Pva * abs(cosPhi); // Puissance active P = S * |cos(phi)|

// Mise à jour des variables globales selon le signe de COSPHI
if (cosPhi >= 0) {
// COSPHI positif = Consommation
PuissanceS_M_inst = PwAbs;
PuissanceI_M_inst = 0;
PVAS_M_inst = PfloatMax(Pva);
PVAI_M_inst = 0;
} else {
// COSPHI négatif = Production (injection)
PuissanceS_M_inst = 0;
PuissanceI_M_inst = PwAbs;
PVAI_M_inst = PfloatMax(Pva);
PVAS_M_inst = 0;
}

// Mise à jour des autres paramètres
Tension_M = voltage;
Intensite_M = current;
PowerFactor_M = abs(cosPhi);

// Énergie cumulée (INDEX et IDXINJECT sont en kWh, on convertit en Wh)
// INDEX = énergie soutirée (consommée)
// IDXINJECT = énergie injectée (produite)
energyIndex = ValXml("INDEX" + pinceTag, Wes_Data);
energyInject = ValXml("IDXINJECT" + pinceTag, Wes_Data);

Energie_M_Soutiree = int(energyIndex * 1000);
Energie_M_Injectee = int(energyInject * 1000);

// Validation des données
Pva_valide = true;

// Filtrage de la puissance (fonction existante du routeur)
filtre_puissance();

// Reset du Watchdog
PuissanceRecue = true;
EnergieActiveValide = true;

// Reset LED jaune (indicateur de communication)
if (cptLEDyellow > 30) {
cptLEDyellow = 4;
}
}
Loading