You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

716 lines
25 KiB

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contrôleur Solaire - Console</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 900px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
h2 {
color: #667eea;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
margin-top: 30px;
}
.nav-links {
text-align: center;
margin-top: 30px;
}
.nav-links a {
display: inline-block;
padding: 10px 20px;
background: #95a5a6;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 5px;
transition: background 0.3s;
}
.nav-links a:hover {
background: #7f8c8d;
}
.logout-btn {
display: inline-block;
padding: 8px 20px;
background: #e74c3c;
color: white;
text-decoration: none;
border-radius: 5px;
font-size: 14px;
transition: background 0.3s;
}
.logout-btn:hover {
background: #c0392b;
}
.header-bar {
text-align: right;
margin-bottom: 20px;
}
.status-bar {
background: #f0f0f0;
padding: 10px 15px;
border-radius: 5px;
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #555;
}
.data-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 30px 0;
}
.data-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 15px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.3s;
}
.data-card:hover {
transform: translateY(-5px);
}
.mode-btn {
padding: 15px 25px;
border: 2px solid #667eea;
background: white;
color: #667eea;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
}
.mode-btn:hover {
background: #667eea;
color: white;
}
.mode-btn.active {
background: #667eea;
color: white;
}
.mode-btn.off {
border-color: #e74c3c;
color: #e74c3c;
}
.mode-btn.off.active {
background: #e74c3c;
color: white;
}
.mode-btn.on {
border-color: #27ae60;
color: #27ae60;
}
.mode-btn.on.active {
background: #27ae60;
color: white;
}
.mode-btn.secondary {
background: #95a5a6;
border-color: #95a5a6;
color: white;
}
.mode-btn.secondary:hover {
background: #7f8c8d;
border-color: #7f8c8d;
}
.data-card.solar {
background: linear-gradient(135deg, #ffeaa7 0%, #fdcb6e 100%);
}
.data-card.consumption {
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
color: white;
}
.data-card.heater {
background: linear-gradient(135deg, #ff7675 0%, #d63031 100%);
color: white;
}
.data-card.temperature {
background: linear-gradient(135deg, #a29bfe 0%, #6c5ce7 100%);
color: white;
}
.data-icon {
font-size: 32px;
margin-bottom: 5px;
}
.data-label {
font-size: 14px;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 5px;
opacity: 0.8;
}
.data-value {
font-size: 28px;
font-weight: bold;
margin-bottom: 3px;
}
.data-unit {
font-size: 14px;
opacity: 0.9;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #27ae60;
animation: pulse 2s infinite;
}
.status-dot.red {
background: #e74c3c;
}
.status-dot.orange {
background: #f39c12;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.progress-bar {
width: 100%;
height: 30px;
background: #ecf0f1;
border-radius: 15px;
overflow: hidden;
margin-top: 15px;
position: relative;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #e74c3c 0%, #c0392b 100%);
transition: width 0.5s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 14px;
}
.mode-selector {
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
margin-top: 20px;
}
.help-icon {
display: inline-block;
width: 24px;
height: 24px;
border-radius: 50%;
background: #667eea;
color: white;
text-align: center;
line-height: 24px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
margin-left: 10px;
transition: background 0.3s;
}
.help-icon:hover {
background: #764ba2;
}
.help-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
z-index: 1000;
justify-content: center;
align-items: center;
}
.help-modal.show {
display: flex;
}
.help-content {
background: white;
padding: 30px;
border-radius: 10px;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.help-content h3 {
color: #667eea;
margin-top: 0;
margin-bottom: 20px;
}
.help-content .mode-item {
margin-bottom: 20px;
padding: 15px;
background: #f5f7fa;
border-radius: 5px;
border-left: 4px solid #667eea;
}
.help-content .mode-title {
font-weight: bold;
font-size: 16px;
color: #333;
margin-bottom: 8px;
}
.help-content .mode-desc {
color: #555;
line-height: 1.6;
}
.close-help {
float: right;
font-size: 28px;
font-weight: bold;
color: #aaa;
cursor: pointer;
line-height: 20px;
}
.close-help:hover {
color: #333;
}
</style>
</head>
<body>
<div class="container">
<div class="header-bar">
<a href="/logout" class="logout-btn">🔓 Déconnexion</a>
</div>
<h1> Contrôleur Solaire pour Chauffe-Eau</h1>
<div id="statusBar" class="status-bar">
<div class="status-indicator">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Chargement...</span>
</div>
<div style="display: flex; gap: 18px; align-items: center;">
<span id="sunriseStatus">🌅 --:--</span>
<span id="sunsetStatus">🌇 --:--</span>
<span id="lastUpdate">Dernière mise à jour: --</span>
</div>
</div>
<div id="dataDisplay" style="display:none;">
<h2>📊 Données en temps réel</h2>
<div class="data-grid">
<div class="data-card solar">
<div class="data-icon"></div>
<div class="data-label">Production Solaire</div>
<div class="data-value" id="solarValue">--</div>
<div class="data-unit">Watts</div>
</div>
<div class="data-card consumption">
<div class="data-icon"></div>
<div class="data-label">Consommation</div>
<div class="data-value" id="consumptionValue">--</div>
<div class="data-unit">Watts</div>
</div>
<div class="data-card heater">
<div class="data-icon">🔥</div>
<div class="data-label">Puissance Chauffe-Eau</div>
<div class="data-value" id="heaterValue">--</div>
<div class="data-unit">%</div>
<div class="progress-bar">
<div class="progress-fill" id="heaterProgress" style="width: 0%"></div>
</div>
</div>
<div class="data-card temperature">
<div class="data-icon">🌡</div>
<div class="data-label">Température Eau</div>
<div class="data-value" id="temperatureValue">--</div>
<div class="data-unit">°C</div>
</div>
</div>
<h2>🎛 Mode de Fonctionnement <span class="help-icon" onclick="toggleHelp()">?</span></h2>
<div class="mode-selector">
<button class="mode-btn" data-bit="16" onclick="toggleMode(16)">🟢 ON</button>
<button class="mode-btn" data-bit="8" onclick="toggleMode(8)">🌙 NUIT</button>
<button class="mode-btn" data-bit="4" onclick="toggleMode(4)"> SOLEIL</button>
<button class="mode-btn" data-bit="2" onclick="toggleMode(2)">☼ JOUR</button>
<button class="mode-btn" data-bit="0" onclick="toggleMode(0)">🔴 OFF</button>
</div>
<div class="nav-links">
<a href="/settings.html" > Configuration</a>
<a href="/update.html" >📦 Mise à jour OTA</a>
</div>
</div>
</div>
<!-- Modal d'aide -->
<div id="helpModal" class="help-modal" onclick="closeHelp(event)">
<div class="help-content" onclick="event.stopPropagation()">
<span class="close-help" onclick="toggleHelp()">&times;</span>
<h3>📖 Guide des Modes de Chauffe</h3>
<div class="mode-item">
<div class="mode-title">🟢 Mode ON</div>
<div class="mode-desc">
Le chauffe-eau fonctionne à pleine puissance en permanence, indépendamment de la production solaire.
</div>
</div>
<div class="mode-item">
<div class="mode-title">🔴 Mode OFF</div>
<div class="mode-desc">
Le chauffe-eau est complètement désactivé. Aucune chauffe n'est effectuée.
</div>
</div>
<div class="mode-item">
<div class="mode-title">🌙 Mode NUIT</div>
<div class="mode-desc">
Active le chauffage entre <span id="nightHoursHelp">0h - 4h</span> pour atteindre
la température minimale configurée de <span id="minTempHelp">40°C</span>.<br/>(Ces valeurs peuvent être modifiées dans les paramètres)
</div>
</div>
<div class="mode-item">
<div class="mode-title"> Mode SOLEIL</div>
<div class="mode-desc">
Utilise uniquement l'excédent de production solaire pour chauffer l'eau. La puissance de chauffe
s'ajuste automatiquement en fonction de la production solaire disponible. Optimise l'autoconsommation.
</div>
</div>
<div class="mode-item">
<div class="mode-title">☼ Mode JOUR</div>
<div class="mode-desc">
Chauffe intelligemment pendant les heures d'ensoleillement (lever à coucher du soleil) pour atteindre
la température maximale configurée de<span id="maxTempHelp">60°C</span> à l'heure cible. La puissance est calculée en fonction du temps
restant et de l'écart de température. Le chauffage s'arrête automatiquement une fois la température
maximale atteinte jusqu'au lendemain. <em>(Le bouton devient gris quand la température max est atteinte)</em>
</div>
</div>
<div class="mode-item">
<div class="mode-title">🔄 Combinaisons</div>
<div class="mode-desc">
Vous pouvez combiner les modes NUIT, SOLEIL et JOUR pour une gestion optimale :
<ul style="margin: 10px 0 0 20px;">
<li><strong>NUIT + SOLEIL</strong> : Maintien nocturne + optimisation solaire diurne</li>
<li><strong>NUIT + JOUR</strong> : Maintien nocturne + chauffe intelligente diurne</li>
<li><strong>SOLEIL + JOUR</strong> : Utilise l'excédent + planifie la chauffe</li>
<li><strong>NUIT + SOLEIL + JOUR</strong> : Combinaison complète pour un fonctionnement 24h/24</li>
</ul>
</div>
</div>
</div>
</div>
<script>
let refreshInterval;
let currentMode = 0; // Mode par défaut (OFF)
let configNightStart = 0;
let configNightEnd = 4;
let configMinTemp = 40;
let configMaxTemp = 60;
function toggleHelp() {
const modal = document.getElementById('helpModal');
modal.classList.toggle('show');
// Mettre à jour les heures affichées dans le modal
if (modal.classList.contains('show')) {
document.getElementById('nightHoursHelp').textContent =
configNightStart + 'h - ' + configNightEnd + 'h';
document.getElementById('minTempHelp').textContent = configMinTemp + '°C';
document.getElementById('maxTempHelp').textContent = configMaxTemp + '°C';
}
}
function closeHelp(event) {
if (event.target.id === 'helpModal') {
toggleHelp();
}
}
function showError(message) {
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
statusDot.className = 'status-dot red';
statusText.textContent = message;
setTimeout(() => {
updateStatusBar();
}, 5000);
}
function updateStatusBar() {
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
// Vérifier l'état d'erreur Enphase
if (window.enphaseConnectionError) {
statusDot.className = 'status-dot orange';
statusText.textContent = 'Erreur connexion Enphase';
} else {
statusDot.className = 'status-dot';
statusText.textContent = 'Connecté';
}
}
function updateModeDisplay() {
// Mettre à jour l'affichage des boutons actifs
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.classList.remove('active', 'on', 'off');
});
if (currentMode === 0) {
// Mode OFF
document.querySelector('[data-bit="0"]').classList.add('active', 'off');
} else if (currentMode === 16) {
// Mode ON
document.querySelector('[data-bit="16"]').classList.add('active', 'on');
} else {
// Combinaisons NUIT, SOLEIL, JOUR
if (currentMode & 8) {
document.querySelector('[data-bit="8"]').classList.add('active');
}
if (currentMode & 4) {
document.querySelector('[data-bit="4"]').classList.add('active');
}
if (currentMode & 2) {
document.querySelector('[data-bit="2"]').classList.add('active');
}
}
}
function toggleMode(bitValue) {
let newMode;
if (bitValue === 16) {
// Bouton ON: activer uniquement ON (16)
newMode = 16;
} else if (bitValue === 0) {
// Bouton OFF: désactiver tout (0)
newMode = 0;
} else {
// Boutons NUIT (8), SOLEIL (4), JOUR (2)
// Si ON ou OFF est actif, commencer avec 0
if (currentMode === 16 || currentMode === 0) {
newMode = bitValue;
} else {
// Toggle le bit
newMode = currentMode ^ bitValue;
// Si tout est désactivé, mettre à 0 (OFF)
if (newMode === 0) {
newMode = 0;
}
}
}
currentMode = newMode;
updateModeDisplay();
// Envoyer le mode au serveur
fetch('/api/mode', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ mode: currentMode })
})
.then(response => {
if (response.status === 401) {
// Session expirée, rediriger vers login
window.location.href = '/login';
return;
}
if (!response.ok) {
throw new Error('Erreur lors du changement de mode');
}
return response.json();
})
.then(data => {
if (!data) return; // Éviter l'erreur si redirection 401
console.log('Mode changé:', currentMode);
})
.catch(error => {
console.error('Erreur:', error);
showError('Erreur lors du changement de mode');
});
}
function loadMode() {
fetch('/api/mode')
.then(response => {
if (response.status === 401) {
// Session expirée, rediriger vers login
window.location.href = '/login';
return;
}
return response.json();
})
.then(data => {
if (!data) return; // Éviter l'erreur si redirection 401
if (typeof data.mode === 'number') {
currentMode = data.mode;
updateModeDisplay();
}
})
.catch(error => {
console.error('Erreur chargement mode:', error);
});
}
function updateData() {
fetch('/api/data')
.then(response => {
if (response.status === 401) {
window.location.href = '/login';
return;
}
if (!response.ok) {
throw new Error('Erreur réseau');
}
return response.json();
})
.then(data => {
if (!data) return;
// Mettre à jour le flag global d'erreur Enphase
window.enphaseConnectionError = !!data.enphase_connection_error;
updateStatusBar();
// Mettre à jour les heures de configuration
if (data.night_start_hour !== undefined) {
configNightStart = data.night_start_hour;
}
if (data.night_end_hour !== undefined) {
configNightEnd = data.night_end_hour;
}
if (data.min_water_temp !== undefined) {
configMinTemp = data.min_water_temp;
}
if (data.max_water_temp !== undefined) {
configMaxTemp = data.max_water_temp;
}
// Mettre à jour le style du bouton JOUR si maxTempReachedToday
const jourBtn = document.querySelector('[data-bit="2"]');
if (data.max_temp_reached_today && jourBtn.classList.contains('active')) {
jourBtn.classList.add('secondary');
} else {
jourBtn.classList.remove('secondary');
}
document.getElementById('dataDisplay').style.display = 'block';
// Mettre à jour la production solaire
document.getElementById('solarValue').textContent =
data.solar_production.toFixed(0);
// Mettre à jour la consommation
document.getElementById('consumptionValue').textContent =
data.power_consumption.toFixed(0);
// Mettre à jour la puissance du chauffe-eau
document.getElementById('heaterValue').textContent =
data.heater_power;
// Mettre à jour la température de l'eau
document.getElementById('temperatureValue').textContent =
data.water_temperature ? data.water_temperature.toFixed(1) : '--';
// Mettre à jour les heures de lever/coucher du soleil
document.getElementById('sunriseStatus').textContent = '🌅 ' + (data.sunrise_time || '--:--');
document.getElementById('sunsetStatus').textContent = '🌇 ' + (data.sunset_time || '--:--');
// Mettre à jour la barre de progression
const progressBar = document.getElementById('heaterProgress');
progressBar.style.width = data.heater_power + '%';
// Mettre à jour l'heure de dernière mise à jour
const now = new Date();
const timeString = now.toLocaleTimeString('fr-FR');
document.getElementById('lastUpdate').textContent =
'Dernière mise à jour: ' + timeString;
})
.catch(error => {
console.error('Erreur:', error);
showError('Erreur lors de la récupération des données');
});
}
// Charger les données au démarrage
window.addEventListener('DOMContentLoaded', () => {
loadMode(); // Charger le mode actuel
updateData();
// Rafraîchir toutes les 2 secondes
refreshInterval = setInterval(updateData, 2000);
});
// Nettoyer l'intervalle quand on quitte la page
window.addEventListener('beforeunload', () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
</script>
</body>
</html>