Files
scheianu 1152f93a00 Add Olimex ESP32-C6-EVB + PN532 NFC driver and web UI
- New driver: app/drivers/olimex_esp32_c6_evb_pn532/
  - Full relay/input control (inherits base behaviour)
  - get_nfc_status(), get_nfc_config(), set_nfc_config() methods
  - manifest.json with NFC hardware metadata (UEXT1 pins, card standard)

- NFC management routes (boards.py):
  - GET  /boards/<id>/nfc            — management page
  - GET  /boards/<id>/nfc/status_json — live JSON (polled every 3 s)
  - POST /boards/<id>/nfc/config     — save auth UID / relay / timeout
  - POST /boards/<id>/nfc/enroll     — enrol last-seen card with one click

- New template: templates/boards/nfc.html
  - Live reader status (PN532 ready, access state, last UID)
  - Quick Enroll: present card → Refresh → Enrol in one click
  - Manual Settings: type/paste UID, pick relay, set absence timeout

- detail.html: NFC Access Control button shown for pn532 board type
2026-03-15 09:41:01 +02:00

211 lines
9.7 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}{{ board.name }} Location Management{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('boards.list_boards') }}">Boards</a></li>
<li class="breadcrumb-item active">{{ board.name }}</li>
</ol>
</nav>
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
<h2 class="fw-bold mb-0">{{ board.name }}</h2>
<span class="badge text-bg-secondary">{{ board.board_type }}</span>
<span class="badge {% if board.is_online %}text-bg-success{% else %}text-bg-secondary{% endif %} ms-1">
{% if board.is_online %}Online{% else %}Offline{% endif %}
</span>
<span class="text-secondary small ms-2 font-monospace">{{ board.host }}:{{ board.port }}</span>
</div>
{% if current_user.is_admin() %}
<div class="d-flex gap-2">
{% if board.board_type == 'olimex_esp32_c6_evb_pn532' %}
<a href="{{ url_for('boards.nfc_management', board_id=board.id) }}" class="btn btn-outline-primary">
<i class="bi bi-credit-card-2-front me-1"></i> NFC Access Control
</a>
{% endif %}
<a href="{{ url_for('boards.edit_entities', board_id=board.id) }}" class="btn btn-outline-info">
<i class="bi bi-palette me-1"></i> Configure Entities
</a>
<a href="{{ url_for('boards.edit_board', board_id=board.id) }}" class="btn btn-outline-secondary">
<i class="bi bi-pencil me-1"></i> Edit
</a>
</div>
{% endif %}
</div>
<div class="row g-4">
<!-- ── Relay controls ────────────────────────────────────────────────────── -->
<div class="col-lg-6">
<div class="card border-0 rounded-4 h-100">
<div class="card-header bg-transparent fw-semibold pt-3">
<i class="bi bi-lightning-charge me-1 text-warning"></i> Relay Controls
</div>
<div class="card-body">
<div class="row g-3">
{% for n in range(1, board.num_relays + 1) %}
{% set relay_key = "relay_" ~ n %}
{% set is_on = board.relay_states.get(relay_key, false) %}
{% set e = board.get_relay_entity(n) %}
<div class="col-6">
<div id="relay-card-{{ n }}" class="card rounded-3 border-{{ e.on_color if is_on else e.off_color }} h-100">
<div class="card-body text-center py-3">
<i id="relay-icon-{{ n }}" class="bi {{ e.icon }} mb-2 text-{{ e.on_color if is_on else e.off_color }}"
style="font-size:2rem"></i>
<div class="fs-6 fw-semibold mb-1">{{ e.name }}</div>
<div class="mb-3">
<span id="relay-badge-{{ n }}" class="badge text-bg-{{ e.on_color if is_on else e.off_color }}">
{{ e.on_label if is_on else e.off_label }}
</span>
</div>
<div class="d-flex justify-content-center gap-2">
<form method="POST" action="{{ url_for('boards.set_relay_view', board_id=board.id, relay_num=n) }}">
<button id="relay-on-btn-{{ n }}" class="btn btn-sm btn-{{ e.on_color }}" {% if is_on %}disabled{% endif %}>{{ e.on_label }}</button>
<input type="hidden" name="state" value="on" />
</form>
<form method="POST" action="{{ url_for('boards.toggle_relay_view', board_id=board.id, relay_num=n) }}">
<button class="btn btn-sm btn-outline-primary">Toggle</button>
</form>
<form method="POST" action="{{ url_for('boards.set_relay_view', board_id=board.id, relay_num=n) }}">
<button id="relay-off-btn-{{ n }}" class="btn btn-sm btn-{{ e.off_color }}" {% if not is_on %}disabled{% endif %}>{{ e.off_label }}</button>
<input type="hidden" name="state" value="off" />
</form>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- ── Input states ──────────────────────────────────────────────────────── -->
<div class="col-lg-6">
<div class="card border-0 rounded-4 h-100">
<div class="card-header bg-transparent fw-semibold pt-3">
<i class="bi bi-activity me-1 text-info"></i> Input States
</div>
<div class="card-body">
<div class="row g-3">
{% for n in range(1, board.num_inputs + 1) %}
{% set input_key = "input_" ~ n %}
{% set raw_state = board.input_states.get(input_key, true) %}
{% set is_active = not raw_state %}
{% set e = board.get_input_entity(n) %}
<div class="col-6">
<div id="input-card-{{ n }}" class="card rounded-3 border-{{ e.active_color if is_active else e.idle_color }}">
<div class="card-body text-center py-3">
<i id="input-icon-{{ n }}" class="bi {{ e.icon }} mb-2 text-{{ e.active_color if is_active else e.idle_color }}"
style="font-size:2rem"></i>
<div class="fs-6 fw-semibold mb-1">{{ e.name }}</div>
<span id="input-badge-{{ n }}" class="badge text-bg-{{ e.active_color if is_active else e.idle_color }}">
{{ e.active_label if is_active else e.idle_label }}
</span>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- ── Board info ────────────────────────────────────────────────────────── -->
<div class="col-12">
<div class="card border-0 rounded-4">
<div class="card-header bg-transparent fw-semibold pt-3">
<i class="bi bi-info-circle me-1"></i> Board Information
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-3">Board ID</dt><dd class="col-sm-9 font-monospace">{{ board.id }}</dd>
<dt class="col-sm-3">Type</dt><dd class="col-sm-9">{{ board.board_type }}</dd>
<dt class="col-sm-3">Host</dt><dd class="col-sm-9 font-monospace">{{ board.host }}:{{ board.port }}</dd>
<dt class="col-sm-3">Firmware</dt><dd class="col-sm-9">{{ board.firmware_version or '—' }}</dd>
<dt class="col-sm-3">Added</dt><dd class="col-sm-9">{{ board.created_at.strftime('%Y-%m-%d %H:%M') }}</dd>
<dt class="col-sm-3">Last Seen</dt>
<dd class="col-sm-9">{% if board.last_seen %}{{ board.last_seen.strftime('%Y-%m-%d %H:%M:%S') }}{% else %}Never{% endif %}</dd>
</dl>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const socket = io();
const boardId = {{ board.id }};
// ── Entity config embedded from server ──────────────────────────────────────
const ENTITY_CONFIG = {
relays: {
{% for n in range(1, board.num_relays + 1) %}{% set e = board.get_relay_entity(n) %}
{{ n }}: {icon:"{{ e.icon }}",onColor:"{{ e.on_color }}",offColor:"{{ e.off_color }}",onLabel:"{{ e.on_label }}",offLabel:"{{ e.off_label }}"},
{% endfor %}
},
inputs: {
{% for n in range(1, board.num_inputs + 1) %}{% set e = board.get_input_entity(n) %}
{{ n }}: {icon:"{{ e.icon }}",activeColor:"{{ e.active_color }}",idleColor:"{{ e.idle_color }}",activeLabel:"{{ e.active_label }}",idleLabel:"{{ e.idle_label }}"},
{% endfor %}
}
};
function swapBorderColor(el, color) {
el.classList.forEach(c => {
if (c.startsWith('border-') && c !== 'border-0' && c !== 'border-3') el.classList.remove(c);
});
el.classList.add('border-' + color);
}
function applyRelayState(n, isOn) {
const e = ENTITY_CONFIG.relays[n]; if (!e) return;
const color = isOn ? e.onColor : e.offColor;
const label = isOn ? e.onLabel : e.offLabel;
const card = document.getElementById('relay-card-' + n);
const icon = document.getElementById('relay-icon-' + n);
const badge = document.getElementById('relay-badge-' + n);
const onBtn = document.getElementById('relay-on-btn-' + n);
const offBtn= document.getElementById('relay-off-btn-' + n);
if (card) swapBorderColor(card, color);
if (icon) { icon.className = `bi ${e.icon} mb-2 text-${color}`; icon.style.fontSize = '2rem'; }
if (badge) { badge.className = 'badge text-bg-' + color; badge.textContent = label; }
if (onBtn) onBtn.disabled = isOn;
if (offBtn) offBtn.disabled = !isOn;
}
function applyInputState(n, rawState) {
const e = ENTITY_CONFIG.inputs[n]; if (!e) return;
const isActive = !rawState; // NC contact: raw true = resting = idle
const color = isActive ? e.activeColor : e.idleColor;
const label = isActive ? e.activeLabel : e.idleLabel;
const card = document.getElementById('input-card-' + n);
const icon = document.getElementById('input-icon-' + n);
const badge = document.getElementById('input-badge-' + n);
if (card) swapBorderColor(card, color);
if (icon) { icon.className = `bi ${e.icon} mb-2 text-${color}`; icon.style.fontSize = '2rem'; }
if (badge) { badge.className = 'badge text-bg-' + color; badge.textContent = label; }
}
socket.on("board_update", function(data) {
if (data.board_id !== boardId) return;
if (data.relay_states) {
for (const [key, isOn] of Object.entries(data.relay_states)) {
applyRelayState(parseInt(key.split('_')[1]), isOn);
}
}
if (data.input_states) {
for (const [key, rawState] of Object.entries(data.input_states)) {
applyInputState(parseInt(key.split('_')[1]), rawState);
}
}
});
</script>
{% endblock %}