feat: Home Assistant integration — live status + auto-sync print slots
Deploy via Portainer / deploy (push) Successful in 0s

This commit is contained in:
Jo
2026-07-07 11:40:52 +02:00
parent 4206a8934e
commit 5686706a4e
5 changed files with 281 additions and 6 deletions
+75
View File
@@ -92,6 +92,21 @@
</div>
</div>
</div>
<!-- Live HA Status -->
<div class="col-md-5">
<div class="card">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<span><i class="bi bi-activity me-2 text-success"></i>Imprimantes en direct</span>
<span id="haLastUpdate" class="text-muted" style="font-size:.7rem"></span>
</div>
<div class="card-body p-2" id="haPrinterCards">
<div class="text-center text-muted py-3 small">
<i class="bi bi-hourglass-split me-1"></i>Chargement…
</div>
</div>
</div>
</div>
</div>
<!-- Modal planification -->
@@ -537,5 +552,65 @@ async function deleteSlot() {
}
initTimeline();
// ── Home Assistant live status ──────────────────────────────────────────────
async function refreshHaStatus() {
try {
const r = await fetch('/api/ha/status');
const d = await r.json();
const el = document.getElementById('haPrinterCards');
const lu = document.getElementById('haLastUpdate');
lu.textContent = 'Mis à jour ' + new Date().toLocaleTimeString('fr-FR', {hour:'2-digit',minute:'2-digit'});
if (d.error && !d.printers) {
el.innerHTML = `<div class="text-muted small p-3"><i class="bi bi-info-circle me-1"></i>${d.error}</div>`;
return;
}
const printers = d.printers || {};
const keys = Object.keys(printers);
if (!keys.length) {
el.innerHTML = '<div class="text-muted small p-3"><i class="bi bi-info-circle me-1"></i>Aucune imprimante avec préfixe HA. Configurez dans Paramètres + Imprimantes.</div>';
return;
}
const STATUS_LABEL = {running:'En cours',finish:'Terminé',idle:'Idle',pause:'En pause',failed:'Erreur',offline:'Hors ligne',init:'Démarrage',unknown:'—'};
const STATUS_CLS = {running:'success',finish:'secondary',idle:'light',pause:'warning',failed:'danger',offline:'dark',init:'info',unknown:'light'};
el.innerHTML = keys.map(pid => {
const p = printers[pid];
const sc = STATUS_CLS[p.status] || 'light';
const sl = STATUS_LABEL[p.status] || p.status;
const prog = p.progress || 0;
const barColor = p.error ? '#ef4444' : p.status === 'running' ? '#22c55e' : '#94a3b8';
const layerInfo = (p.layers_total > 0)
? `<span class="text-muted" style="font-size:.7rem">Couche ${p.layer}/${p.layers_total}</span>`
: '';
const finInfo = (p.heure_fin && p.heure_fin !== 'unknown' && p.status === 'running')
? `<span class="text-muted" style="font-size:.7rem">Fin ≈ ${new Date(p.heure_fin).toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit'})}</span>`
: '';
const taskInfo = (p.task_name && p.task_name !== 'unknown')
? `<div class="text-truncate" style="font-size:.72rem;color:#6b7280;max-width:220px" title="${p.task_name}">${p.task_name}</div>`
: '';
return `<div class="p-2 mb-1 rounded" style="background:#f8f9fa;border:1px solid #e5e7eb">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-semibold small">${p.printer_name}</span>
<span class="badge bg-${sc} text-${sc==='light'?'dark':'white'}">${sl}</span>
</div>
${taskInfo}
<div class="d-flex justify-content-between align-items-center mt-1">
<div class="flex-fill me-2">
<div style="height:6px;background:#e5e7eb;border-radius:3px;overflow:hidden">
<div style="width:${prog}%;height:100%;background:${barColor};transition:width .5s"></div>
</div>
</div>
<span class="fw-bold small" style="min-width:36px;text-align:right">${prog}%</span>
</div>
<div class="d-flex gap-3 mt-1">${layerInfo}${finInfo}</div>
</div>`;
}).join('');
} catch(e) {
console.warn('HA status error:', e);
}
}
refreshHaStatus();
setInterval(refreshHaStatus, 30000);
</script>
{% endblock %}
+14 -1
View File
@@ -15,7 +15,7 @@
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr><th>Nom</th><th>Statut</th><th>Notes</th><th></th></tr>
<tr><th>Nom</th><th>Statut</th><th>Préfixe HA</th><th>Notes</th><th></th></tr>
</thead>
<tbody>
{% for p in printers %}
@@ -28,6 +28,13 @@
<span class="badge bg-secondary">Inactive</span>
{% endif %}
</td>
<td>
{% if p.ha_entity_prefix %}
<code class="small text-primary">{{ p.ha_entity_prefix }}</code>
{% else %}
<span class="text-muted small"></span>
{% endif %}
</td>
<td class="text-muted small">{{ p.notes or '—' }}</td>
<td>
<div class="d-flex gap-1">
@@ -59,6 +66,12 @@
<label class="form-label fw-semibold">Nom</label>
<input type="text" name="name" class="form-control" required placeholder="ex: A1 #7, X1C #1">
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Préfixe entité Home Assistant <span class="text-muted fw-normal small">(optionnel)</span></label>
<input type="text" name="ha_entity_prefix" class="form-control font-monospace"
placeholder="ex : patabambulab01">
<div class="form-text">Préfixe HA pour la synchro automatique du planning. Configurer l'URL HA dans <a href="{{ url_for('settings') }}">Paramètres</a>.</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Notes</label>
<input type="text" name="notes" class="form-control" placeholder="couleur, emplacement…">
+53
View File
@@ -239,6 +239,42 @@
</div>
</div>
<!-- Home Assistant -->
<div class="col-12">
<div class="card">
<div class="card-header py-3"><i class="bi bi-house-heart me-2"></i>Home Assistant — Sync imprimantes</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">URL Home Assistant</label>
<input type="url" name="ha_url" class="form-control"
placeholder="https://homeassistant.tondomain.com"
value="{{ settings.get('ha_url','') }}">
<div class="form-text">Sans slash final. Ex : <code>https://homeassistant.lespatas.ovh</code></div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Token longue durée</label>
<input type="password" name="ha_token" class="form-control font-monospace"
placeholder="••••••••••••••••••••"
value="{{ settings.get('ha_token','') }}">
<div class="form-text">Profil HA → Sécurité → Jetons d'accès longue durée.</div>
</div>
<div class="col-12">
<button type="button" class="btn btn-sm btn-outline-secondary" id="testHaBtn">
<i class="bi bi-plug me-1"></i>Tester la connexion HA
</button>
<span id="testHaResult" class="ms-3 small"></span>
<div class="form-text mt-1">
<i class="bi bi-info-circle me-1"></i>
Configurez ensuite le <strong>Préfixe entité HA</strong> pour chaque imprimante (ex&nbsp;: <code>patabambulab01</code>) dans la page <a href="{{ url_for('printers_page') }}">Imprimantes</a>.
Le planning se synchronise alors automatiquement toutes les 2&nbsp;min.
</div>
</div>
</div>
</div>
</div>
</div>
<!-- iCal -->
<div class="col-12">
<div class="card">
@@ -307,5 +343,22 @@ document.getElementById('testNcBtn').addEventListener('click', async function ()
res.innerHTML = `<span class="text-danger"><i class="bi bi-x-circle me-1"></i>${e.message}</span>`;
}
});
document.getElementById('testHaBtn').addEventListener('click', async function () {
const res = document.getElementById('testHaResult');
res.textContent = 'Test en cours…';
try {
const r = await fetch('/api/ha/status');
const d = await r.json();
if (d.error) {
res.innerHTML = `<span class="text-danger"><i class="bi bi-x-circle me-1"></i>${d.error}</span>`;
} else {
const count = Object.keys(d.printers).length;
res.innerHTML = `<span class="text-success"><i class="bi bi-check-circle me-1"></i>Connexion OK — ${count} imprimante(s) avec préfixe HA</span>`;
}
} catch (e) {
res.innerHTML = `<span class="text-danger"><i class="bi bi-x-circle me-1"></i>${e.message}</span>`;
}
});
</script>
{% endblock %}