|
|
<!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; |
|
|
} |
|
|
|
|
|
.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; |
|
|
} |
|
|
</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</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> |
|
|
|
|
|
<script> |
|
|
let refreshInterval; |
|
|
let currentMode = 0; // Mode par défaut (OFF) |
|
|
|
|
|
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(); |
|
|
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>
|
|
|
|