Files
location_managemet/app/templates/boards/add.html
ske087 1e89323035 Add Tuya Cloud integration; 2-step add-board wizard; DP value formatting
- Tuya Cloud Gateway driver (tuya-device-sharing-sdk):
  - QR-code auth flow: user-provided user_code, terminal_id/endpoint
    returned by Tuya login_result (mirrors HA implementation)
  - Device sync, toggle DP, rename, per-device detail page
  - category → kind mapping; detect_switch_dps helper
  - format_dp_value: temperature (÷10 + °C/°F), humidity (+ %)
    registered as 'tuya_dp' Jinja2 filter

- TuyaDevice model (tuya_devices table)

- Templates:
  - tuya/gateway.html: device grid with live-reading sensor cards
    (config/threshold keys hidden from card, shown on detail page)
  - tuya/device.html: full status table with formatted DP values
  - tuya/auth_settings.html: user_code input + QR scan flow

- Add-board wizard refactored to 2-step flow:
  - Step 1: choose board type (Cloud Gateways vs Hardware)
  - Step 2: type-specific fields; gateways skip IP/relay fields

- Layout builder: Tuya chip support (makeTuyaChip, tuya_update socket)

- requirements.txt: tuya-device-sharing-sdk, cryptography
2026-02-27 16:06:48 +02:00

342 lines
13 KiB
HTML
Raw 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 %}Add Board Location Management{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('boards.list_boards') }}">Boards</a></li>
<li class="breadcrumb-item active">Add Board</li>
</ol>
</nav>
{# ── Step indicator ──────────────────────────────────────────────────────── #}
<div class="d-flex align-items-center mb-4" id="step-indicator" style="max-width:420px">
<div class="step-bubble active" id="bubble-1">1</div>
<span class="small fw-semibold ms-2 me-3" id="lbl-1">Choose Type</span>
<div class="step-line" id="step-line"></div>
<div class="step-bubble ms-3 me-2" id="bubble-2">2</div>
<span class="small text-muted" id="lbl-2">Configure</span>
</div>
{# ══════════════════════════════════════════════════════════
STEP 1 pick a board / integration type
══════════════════════════════════════════════════════════ #}
<div id="step1">
<h5 class="fw-bold mb-1">What are you adding?</h5>
<p class="text-muted small mb-4">Choose the hardware board or cloud service you want to connect.</p>
<p class="text-uppercase text-muted fw-semibold small mb-2" style="letter-spacing:.07em">
<i class="bi bi-cloud me-1"></i>Cloud Gateways
</p>
<div class="row g-3 mb-4">
<div class="col-12 col-sm-6">
<button type="button" class="type-card w-100 text-start" data-type="sonoff_ewelink">
<div class="d-flex align-items-start gap-3">
<div class="type-icon" style="background:rgba(255,210,0,.12);color:#ffd200">
<i class="bi bi-cloud-lightning-fill"></i>
</div>
<div>
<div class="fw-semibold">Sonoff eWeLink</div>
<div class="small text-muted mt-1">Control Sonoff devices via the eWeLink cloud.</div>
</div>
</div>
</button>
</div>
<div class="col-12 col-sm-6">
<button type="button" class="type-card w-100 text-start" data-type="tuya_cloud">
<div class="d-flex align-items-start gap-3">
<div class="type-icon" style="background:rgba(32,201,151,.12);color:#20c997">
<i class="bi bi-cloud-fill"></i>
</div>
<div>
<div class="fw-semibold">Tuya Cloud</div>
<div class="small text-muted mt-1">Control Tuya / Smart Life devices via QR login.</div>
</div>
</div>
</button>
</div>
</div>
<p class="text-uppercase text-muted fw-semibold small mb-2" style="letter-spacing:.07em">
<i class="bi bi-cpu me-1"></i>Hardware Boards
</p>
<div class="row g-3">
{% for d in drivers %}
{% if d.DRIVER_ID not in ("sonoff_ewelink", "tuya_cloud") %}
<div class="col-12 col-sm-6 col-lg-4">
<button type="button" class="type-card w-100 text-start" data-type="{{ d.DRIVER_ID }}">
<div class="d-flex align-items-start gap-3">
<div class="type-icon" style="background:rgba(88,166,255,.12);color:#58a6ff">
<i class="bi bi-cpu-fill"></i>
</div>
<div>
<div class="fw-semibold">{{ d.DRIVER_ID | replace('_',' ') | title }}</div>
<div class="small text-muted mt-1">{{ d.DESCRIPTION }}</div>
</div>
</div>
</button>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{# ══════════════════════════════════════════════════════════
STEP 2 configuration form
══════════════════════════════════════════════════════════ #}
<div id="step2" class="d-none" style="max-width:560px">
<button type="button" class="btn btn-link btn-sm text-muted ps-0 mb-3" id="btn-back">
<i class="bi bi-arrow-left me-1"></i>Back
</button>
{# Selected type badge #}
<div class="d-flex align-items-center gap-3 mb-4">
<div class="type-icon-sm" id="s2-icon-wrap"></div>
<div>
<div class="fw-bold fs-6" id="s2-type-label"></div>
<div class="small text-muted" id="s2-type-desc"></div>
</div>
</div>
<form method="POST" id="add-form">
<input type="hidden" name="board_type" id="f-board-type">
{# Name always shown #}
<div class="mb-4">
<label class="form-label fw-semibold">Board Name</label>
<input type="text" name="name" id="f-name" class="form-control"
placeholder="e.g. Living Room Gateway" required autofocus>
<div class="form-text text-muted">A friendly name to identify this board.</div>
</div>
{# ── Gateway info box (Sonoff / Tuya) ── #}
<div id="f-gw-section" class="d-none mb-4">
<div class="alert py-3 d-flex gap-3 align-items-start" id="f-gw-alert">
<span id="f-gw-icon" class="fs-4 mt-1"></span>
<div id="f-gw-text"></div>
</div>
</div>
{# ── Hardware-only fields ── #}
<div id="f-hw-section" class="d-none">
<div class="row g-3 mb-3">
<div class="col-8">
<label class="form-label fw-semibold">IP Address / Hostname</label>
<input type="text" name="host" id="f-host" class="form-control font-monospace"
placeholder="192.168.1.100">
</div>
<div class="col-4">
<label class="form-label fw-semibold">Port</label>
<input type="number" name="port" class="form-control" value="80" min="1" max="65535">
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-6">
<label class="form-label fw-semibold">Relay Outputs</label>
<input type="number" name="num_relays" id="f-relays" class="form-control"
value="4" min="0" max="32">
</div>
<div class="col-6">
<label class="form-label fw-semibold">Digital Inputs</label>
<input type="number" name="num_inputs" id="f-inputs" class="form-control"
value="4" min="0" max="32">
</div>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-check-lg me-1"></i>Add Board
</button>
<a href="{{ url_for('boards.list_boards') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
<style>
.type-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: .75rem;
padding: 1rem 1.1rem;
cursor: pointer;
transition: border-color .15s, background .15s, box-shadow .15s;
color: inherit;
}
.type-card:hover, .type-card:focus {
border-color: #58a6ff;
background: rgba(88,166,255,.06);
box-shadow: 0 0 0 3px rgba(88,166,255,.12);
outline: none;
}
.type-icon {
width: 44px; height: 44px; border-radius: .55rem;
display: flex; align-items: center; justify-content: center;
font-size: 1.4rem; flex-shrink: 0;
}
.type-icon-sm {
width: 38px; height: 38px; border-radius: .45rem;
display: flex; align-items: center; justify-content: center;
font-size: 1.25rem; flex-shrink: 0;
}
.step-bubble {
width: 28px; height: 28px; border-radius: 50%;
background: #30363d; color: #8b949e;
display: flex; align-items: center; justify-content: center;
font-size: .8rem; font-weight: 700; flex-shrink: 0;
transition: background .2s, color .2s;
}
.step-bubble.active { background: #1f6feb; color: #fff; }
.step-bubble.done { background: #238636; color: #fff; }
.step-line {
flex: 1; height: 2px; background: #30363d;
min-width: 32px; max-width: 60px;
transition: background .2s;
}
.step-line.done { background: #238636; }
</style>
{% endblock %}
{% block scripts %}
<script>
(function () {
const DRIVERS = {
{% for d in drivers %}
"{{ d.DRIVER_ID }}": {
relays: {{ d.DEFAULT_NUM_RELAYS }},
inputs: {{ d.DEFAULT_NUM_INPUTS }},
desc: "{{ d.DESCRIPTION | replace('"', '\\"') }}",
},
{% endfor %}
};
const GW_META = {
sonoff_ewelink: {
label: "Sonoff eWeLink",
iconHtml: '<i class="bi bi-cloud-lightning-fill"></i>',
iconBg: "rgba(255,210,0,.15)",
iconColor: "#ffd200",
alertCls: "alert-warning",
alertIcon: '<i class="bi bi-cloud-lightning-fill text-warning"></i>',
alertBody: '<strong>Sonoff eWeLink Gateway</strong><br>'
+ '<span class="small">After adding, you\'ll enter your eWeLink '
+ 'account credentials to discover and control all your Sonoff devices '
+ 'automatically.</span>',
},
tuya_cloud: {
label: "Tuya Cloud",
iconHtml: '<i class="bi bi-cloud-fill"></i>',
iconBg: "rgba(32,201,151,.15)",
iconColor: "#20c997",
alertCls: "alert-info",
alertIcon: '<i class="bi bi-qr-code text-info"></i>',
alertBody: '<strong>Tuya / Smart Life Cloud Gateway</strong><br>'
+ '<span class="small">After adding, you\'ll scan a QR code with '
+ 'the <strong>Smart Life</strong> or <strong>Tuya Smart</strong> app '
+ 'to link your account and discover all your Tuya devices automatically.</span>',
},
};
const GATEWAY_TYPES = Object.keys(GW_META);
// DOM
const step1 = document.getElementById("step1");
const step2 = document.getElementById("step2");
const btnBack = document.getElementById("btn-back");
const bubble1 = document.getElementById("bubble-1");
const bubble2 = document.getElementById("bubble-2");
const stepLine = document.getElementById("step-line");
const lbl1 = document.getElementById("lbl-1");
const lbl2 = document.getElementById("lbl-2");
const fType = document.getElementById("f-board-type");
const fName = document.getElementById("f-name");
const fHost = document.getElementById("f-host");
const fRelays = document.getElementById("f-relays");
const fInputs = document.getElementById("f-inputs");
const hwSec = document.getElementById("f-hw-section");
const gwSec = document.getElementById("f-gw-section");
const gwAlert = document.getElementById("f-gw-alert");
const gwIcon = document.getElementById("f-gw-icon");
const gwText = document.getElementById("f-gw-text");
const s2Icon = document.getElementById("s2-icon-wrap");
const s2Label = document.getElementById("s2-type-label");
const s2Desc = document.getElementById("s2-type-desc");
// Card clicks
document.querySelectorAll(".type-card").forEach(function (card) {
card.addEventListener("click", function () { goStep2(card.dataset.type); });
});
// Back
btnBack.addEventListener("click", function () {
step2.classList.add("d-none");
step1.classList.remove("d-none");
bubble1.classList.replace("done", "active");
bubble2.classList.remove("active");
stepLine.classList.remove("done");
lbl1.classList.remove("text-success");
lbl2.classList.add("text-muted");
lbl2.classList.remove("fw-semibold");
});
function goStep2(type) {
const drv = DRIVERS[type] || { relays: 4, inputs: 4, desc: "" };
const meta = GW_META[type] || null;
const isGw = GATEWAY_TYPES.includes(type);
fType.value = type;
fRelays.value = drv.relays;
fInputs.value = drv.inputs;
fName.value = "";
// Header icon + label
if (meta) {
s2Icon.innerHTML = meta.iconHtml;
s2Icon.style.background = meta.iconBg;
s2Icon.style.color = meta.iconColor;
s2Label.textContent = meta.label;
} else {
s2Icon.innerHTML = '<i class="bi bi-cpu-fill"></i>';
s2Icon.style.background = "rgba(88,166,255,.15)";
s2Icon.style.color = "#58a6ff";
s2Label.textContent = type.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
}
s2Desc.textContent = drv.desc;
// Section visibility
if (isGw) {
hwSec.classList.add("d-none");
fHost.required = false;
gwSec.classList.remove("d-none");
gwAlert.className = "alert py-3 d-flex gap-3 align-items-start " + meta.alertCls;
gwIcon.innerHTML = meta.alertIcon;
gwText.innerHTML = meta.alertBody;
} else {
hwSec.classList.remove("d-none");
fHost.required = true;
gwSec.classList.add("d-none");
}
// Step indicator
step1.classList.add("d-none");
step2.classList.remove("d-none");
bubble1.classList.remove("active");
bubble1.classList.add("done");
bubble2.classList.add("active");
stepLine.classList.add("done");
lbl1.classList.add("text-success");
lbl2.classList.remove("text-muted");
lbl2.classList.add("fw-semibold");
fName.focus();
}
}());
</script>
{% endblock %}