|
|
|
@ -9,6 +9,8 @@ |
|
|
|
#include <ArduinoJson.h> |
|
|
|
#include <ArduinoJson.h> |
|
|
|
#include <Update.h> |
|
|
|
#include <Update.h> |
|
|
|
#include <ArduinoOTA.h> |
|
|
|
#include <ArduinoOTA.h> |
|
|
|
|
|
|
|
#include <esp_heap_caps.h> |
|
|
|
|
|
|
|
#include <freertos/task.h> |
|
|
|
|
|
|
|
|
|
|
|
// Configuration WiFi
|
|
|
|
// Configuration WiFi
|
|
|
|
String wifiSSID; |
|
|
|
String wifiSSID; |
|
|
|
@ -79,6 +81,11 @@ volatile unsigned long pwmOnTime = 0; // Temps ON en ms pour le cycle actuel |
|
|
|
volatile unsigned long pwmElapsed = 0; // Temps écoulé dans le cycle actuel en ms
|
|
|
|
volatile unsigned long pwmElapsed = 0; // Temps écoulé dans le cycle actuel en ms
|
|
|
|
volatile bool pwmNeedRecalc = true; // Flag pour indiquer qu'il faut recalculer la puissance
|
|
|
|
volatile bool pwmNeedRecalc = true; // Flag pour indiquer qu'il faut recalculer la puissance
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Variables pour la gestion des tâches FreeRTOS (Enphase sur cœur 1)
|
|
|
|
|
|
|
|
TaskHandle_t enphaseTaskHandle = NULL; |
|
|
|
|
|
|
|
unsigned long lastEnphaseUpdate = 0; |
|
|
|
|
|
|
|
bool enphaseUpdateRequested = false; |
|
|
|
|
|
|
|
|
|
|
|
#ifdef ESP32 |
|
|
|
#ifdef ESP32 |
|
|
|
hw_timer_t *timer = NULL; |
|
|
|
hw_timer_t *timer = NULL; |
|
|
|
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED; |
|
|
|
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED; |
|
|
|
@ -90,6 +97,19 @@ AsyncWebServer server(80); |
|
|
|
void handleFileRequest(String path); |
|
|
|
void handleFileRequest(String path); |
|
|
|
void IRAM_ATTR onTimerISR(); |
|
|
|
void IRAM_ATTR onTimerISR(); |
|
|
|
void calculateHeaterPower(); |
|
|
|
void calculateHeaterPower(); |
|
|
|
|
|
|
|
void fetchEnphaseData(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fonction wrapper pour exécuter fetchEnphaseData sur le cœur 1
|
|
|
|
|
|
|
|
void enphaseTaskFunction(void *parameter) { |
|
|
|
|
|
|
|
for (;;) { |
|
|
|
|
|
|
|
// Attendre la notification de mise à jour
|
|
|
|
|
|
|
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Exécuter la récupération des données Enphase sur le cœur 1
|
|
|
|
|
|
|
|
Serial.println("[ENPHASE] Mise à jour sur cœur 1"); |
|
|
|
|
|
|
|
fetchEnphaseData(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Fonction pour créer un fichier config.json par défaut (mode AP)
|
|
|
|
// Fonction pour créer un fichier config.json par défaut (mode AP)
|
|
|
|
bool createDefaultConfig() { |
|
|
|
bool createDefaultConfig() { |
|
|
|
@ -1152,6 +1172,105 @@ void setup() { |
|
|
|
request->send(200, "application/json", response); |
|
|
|
request->send(200, "application/json", response); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// API GET /api/stats - Informations système (CPU, RAM, temps de fonctionnement)
|
|
|
|
|
|
|
|
server.on("/api/stats", HTTP_GET, [](AsyncWebServerRequest *request){ |
|
|
|
|
|
|
|
if (!checkAuthentication(request)) { |
|
|
|
|
|
|
|
request->send(401, "text/plain", "Non autorisé"); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
JsonDocument doc; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Informations de mémoire
|
|
|
|
|
|
|
|
multi_heap_info_t heap_info; |
|
|
|
|
|
|
|
heap_caps_get_info(&heap_info, MALLOC_CAP_DEFAULT); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
size_t totalHeap = heap_caps_get_total_size(MALLOC_CAP_DEFAULT); |
|
|
|
|
|
|
|
size_t freeHeap = heap_caps_get_free_size(MALLOC_CAP_DEFAULT); |
|
|
|
|
|
|
|
size_t usedHeap = totalHeap - freeHeap; |
|
|
|
|
|
|
|
float heapUsagePercent = (usedHeap * 100.0) / totalHeap; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc["memory"]["total_bytes"] = (int)totalHeap; |
|
|
|
|
|
|
|
doc["memory"]["free_bytes"] = (int)freeHeap; |
|
|
|
|
|
|
|
doc["memory"]["used_bytes"] = (int)usedHeap; |
|
|
|
|
|
|
|
doc["memory"]["usage_percent"] = (float)heapUsagePercent; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// PSRAM (si disponible)
|
|
|
|
|
|
|
|
size_t totalPsram = esp_spiram_get_size(); |
|
|
|
|
|
|
|
if (totalPsram > 0) { |
|
|
|
|
|
|
|
size_t freePsram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); |
|
|
|
|
|
|
|
size_t usedPsram = totalPsram - freePsram; |
|
|
|
|
|
|
|
float psramUsagePercent = (usedPsram * 100.0) / totalPsram; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc["psram"]["total_bytes"] = (int)totalPsram; |
|
|
|
|
|
|
|
doc["psram"]["free_bytes"] = (int)freePsram; |
|
|
|
|
|
|
|
doc["psram"]["used_bytes"] = (int)usedPsram; |
|
|
|
|
|
|
|
doc["psram"]["usage_percent"] = (float)psramUsagePercent; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Temps de fonctionnement
|
|
|
|
|
|
|
|
unsigned long uptimeSeconds = millis() / 1000; |
|
|
|
|
|
|
|
unsigned long uptimeDays = uptimeSeconds / 86400; |
|
|
|
|
|
|
|
unsigned long uptimeHours = (uptimeSeconds % 86400) / 3600; |
|
|
|
|
|
|
|
unsigned long uptimeMinutes = (uptimeSeconds % 3600) / 60; |
|
|
|
|
|
|
|
unsigned long uptimeSecs = uptimeSeconds % 60; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc["system"]["uptime_seconds"] = (unsigned long)uptimeSeconds; |
|
|
|
|
|
|
|
doc["system"]["uptime_days"] = (unsigned long)uptimeDays; |
|
|
|
|
|
|
|
doc["system"]["uptime_hours"] = (unsigned long)uptimeHours; |
|
|
|
|
|
|
|
doc["system"]["uptime_minutes"] = (unsigned long)uptimeMinutes; |
|
|
|
|
|
|
|
doc["system"]["uptime_seconds_remainder"] = (unsigned long)uptimeSecs; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fréquence CPU
|
|
|
|
|
|
|
|
doc["system"]["cpu_freq_mhz"] = getCpuFrequencyMhz(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Version du firmware
|
|
|
|
|
|
|
|
doc["system"]["esp_sdk_version"] = ESP.getSdkVersion(); |
|
|
|
|
|
|
|
doc["system"]["sketch_md5"] = ESP.getSketchMD5(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Nombre de tâches FreeRTOS
|
|
|
|
|
|
|
|
doc["system"]["task_count"] = uxTaskGetNumberOfTasks(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Numéro d'expédition (chip ID)
|
|
|
|
|
|
|
|
doc["system"]["chip_revision"] = ESP.getChipRevision(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Adresse MAC
|
|
|
|
|
|
|
|
uint8_t mac[6]; |
|
|
|
|
|
|
|
WiFi.macAddress(mac); |
|
|
|
|
|
|
|
char macStr[18]; |
|
|
|
|
|
|
|
sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); |
|
|
|
|
|
|
|
doc["system"]["mac_address"] = macStr; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Signal WiFi si connecté
|
|
|
|
|
|
|
|
if (WiFi.status() == WL_CONNECTED) { |
|
|
|
|
|
|
|
int rssi = WiFi.RSSI(); |
|
|
|
|
|
|
|
doc["wifi"]["ssid"] = WiFi.SSID(); |
|
|
|
|
|
|
|
doc["wifi"]["rssi_dbm"] = rssi; |
|
|
|
|
|
|
|
doc["wifi"]["signal_strength"] = "excellent"; // Valeur par défaut
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (rssi >= -50) { |
|
|
|
|
|
|
|
doc["wifi"]["signal_strength"] = "excellent"; |
|
|
|
|
|
|
|
} else if (rssi >= -60) { |
|
|
|
|
|
|
|
doc["wifi"]["signal_strength"] = "very good"; |
|
|
|
|
|
|
|
} else if (rssi >= -70) { |
|
|
|
|
|
|
|
doc["wifi"]["signal_strength"] = "good"; |
|
|
|
|
|
|
|
} else if (rssi >= -80) { |
|
|
|
|
|
|
|
doc["wifi"]["signal_strength"] = "fair"; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
doc["wifi"]["signal_strength"] = "weak"; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
doc["wifi"]["connected"] = false; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Uptime des tâches importantes
|
|
|
|
|
|
|
|
doc["tasks"]["enphase_running"] = (enphaseTaskHandle != NULL); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
String response; |
|
|
|
|
|
|
|
serializeJson(doc, response); |
|
|
|
|
|
|
|
request->send(200, "application/json", response); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// API pour ESPEasy - Endpoint simplifié pour recevoir la température
|
|
|
|
// API pour ESPEasy - Endpoint simplifié pour recevoir la température
|
|
|
|
// ESPEasy peut envoyer avec: http://[IP]/api/espeasy?temperature=[value]
|
|
|
|
// ESPEasy peut envoyer avec: http://[IP]/api/espeasy?temperature=[value]
|
|
|
|
server.on("/api/espeasy", HTTP_GET, [](AsyncWebServerRequest *request){ |
|
|
|
server.on("/api/espeasy", HTTP_GET, [](AsyncWebServerRequest *request){ |
|
|
|
@ -1304,7 +1423,7 @@ void setup() { |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (doc.containsKey("mode")) { |
|
|
|
if (doc["mode"].is<int>()) { |
|
|
|
int newMode = doc["mode"].as<int>(); |
|
|
|
int newMode = doc["mode"].as<int>(); |
|
|
|
|
|
|
|
|
|
|
|
// Valider le mode (0-31 pour 5 bits)
|
|
|
|
// Valider le mode (0-31 pour 5 bits)
|
|
|
|
@ -1478,6 +1597,21 @@ void setup() { |
|
|
|
timerAlarmWrite(timer, 10000, true); |
|
|
|
timerAlarmWrite(timer, 10000, true); |
|
|
|
timerAlarmEnable(timer); |
|
|
|
timerAlarmEnable(timer); |
|
|
|
Serial.println("Timer PWM initialisé (100Hz)"); |
|
|
|
Serial.println("Timer PWM initialisé (100Hz)"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Créer la tâche FreeRTOS pour Enphase sur le cœur 1
|
|
|
|
|
|
|
|
// Priority: 2 (plus élevée que tskIDLE_PRIORITY=0 mais moins que le loop=1)
|
|
|
|
|
|
|
|
// StackSize: 4096 octets (environ 2KB pour les variables locales)
|
|
|
|
|
|
|
|
xTaskCreatePinnedToCore( |
|
|
|
|
|
|
|
enphaseTaskFunction, // Fonction de la tâche
|
|
|
|
|
|
|
|
"EnphaseTask", // Nom de la tâche
|
|
|
|
|
|
|
|
8192, // Taille de la pile (8KB pour HTTPClient)
|
|
|
|
|
|
|
|
NULL, // Paramètre
|
|
|
|
|
|
|
|
2, // Priorité (tskIDLE_PRIORITY=0, loop=1)
|
|
|
|
|
|
|
|
&enphaseTaskHandle, // Handle de la tâche
|
|
|
|
|
|
|
|
1 // Cœur 1 (0 = cœur principal, 1 = cœur libre)
|
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Serial.println("Tâche Enphase créée sur cœur 1"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void loop() { |
|
|
|
void loop() { |
|
|
|
@ -1493,13 +1627,18 @@ void loop() { |
|
|
|
// Calculer la puissance du chauffe-eau au début de chaque cycle PWM
|
|
|
|
// Calculer la puissance du chauffe-eau au début de chaque cycle PWM
|
|
|
|
calculateHeaterPower(); |
|
|
|
calculateHeaterPower(); |
|
|
|
|
|
|
|
|
|
|
|
// Récupération des données Enphase
|
|
|
|
// Récupération des données Enphase (non-bloquant avec tâche sur cœur 1)
|
|
|
|
static unsigned long lastEnphaseUpdate = 0; |
|
|
|
|
|
|
|
unsigned long currentMillis = millis(); |
|
|
|
unsigned long currentMillis = millis(); |
|
|
|
|
|
|
|
|
|
|
|
if (currentMillis - lastEnphaseUpdate >= (enphaseUpdateInterval * 1000)) { |
|
|
|
if (currentMillis - lastEnphaseUpdate >= (enphaseUpdateInterval * 1000)) { |
|
|
|
lastEnphaseUpdate = currentMillis; |
|
|
|
lastEnphaseUpdate = currentMillis; |
|
|
|
fetchEnphaseData(); |
|
|
|
|
|
|
|
|
|
|
|
// Notifier la tâche Enphase pour mettre à jour les données
|
|
|
|
|
|
|
|
// Cette opération est non-bloquante et s'exécute sur le cœur 1
|
|
|
|
|
|
|
|
if (enphaseTaskHandle != NULL) { |
|
|
|
|
|
|
|
xTaskNotifyGive(enphaseTaskHandle); |
|
|
|
|
|
|
|
Serial.printf("[LOOP] Mise à jour Enphase déléguée à cœur 1 (interval: %lus)\n", enphaseUpdateInterval); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Recalculer lever/coucher du soleil chaque jour à minuit
|
|
|
|
// Recalculer lever/coucher du soleil chaque jour à minuit
|
|
|
|
|