Files
location_managemet/app/templates/tuya/auth_settings.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

216 lines
7.9 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 %}{{ board.name }} Tuya Settings{% endblock %}
{% block content %}
<div class="container py-4" style="max-width:560px">
<a href="{{ url_for('tuya.gateway', board_id=board.id) }}" class="btn btn-outline-secondary btn-sm mb-3">
<i class="bi bi-arrow-left me-1"></i>Back to Gateway
</a>
<div class="card bg-dark border-secondary">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-qr-code me-2 text-info"></i>Tuya / Smart Life Account
</h5>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} py-2 alert-dismissible fade show" role="alert">
{{ msg }}<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<p class="text-muted small mb-4">
Link your <strong>Tuya / Smart Life</strong> account by scanning a QR code with
the mobile app. No tokens or passwords are stored in plain text only the
access/refresh token pair returned by SSO is persisted.
</p>
{# Current status #}
{% if board.config.get('tuya_token_info') %}
<div class="alert alert-success py-2 d-flex align-items-center gap-2" id="linked-status">
<i class="bi bi-check-circle-fill"></i>
<span>
Account linked.
<small class="opacity-75">(User code: {{ board.config.get('tuya_user_code', '?') }})</small>
</span>
</div>
{% else %}
<div class="alert alert-secondary py-2 d-flex align-items-center gap-2" id="linked-status">
<i class="bi bi-x-circle"></i>
<span>No account linked yet.</span>
</div>
{% endif %}
{# QR section #}
<div id="qr-section">
<div class="mb-3">
<label for="input-user-code" class="form-label fw-semibold">Your Tuya User Code</label>
<input type="text" id="input-user-code" class="form-control"
placeholder="e.g. myname123"
value="{{ board.config.get('tuya_user_code', '') }}">
<div class="form-text">
Choose any short identifier for this installation (letters and numbers, no spaces).
You will need to remember it if you ever re-link the account.
</div>
</div>
<button id="btn-start-qr" class="btn btn-info mb-3">
<i class="bi bi-qr-code me-1"></i>Generate QR Code
</button>
<div id="qr-area" class="text-center d-none">
<div id="qr-canvas" class="d-inline-block p-2 bg-white rounded mb-3"></div>
<p class="text-muted small mb-1" id="qr-instruction">
Open the <strong>Smart Life</strong> (or Tuya Smart) app → tap the
<i class="bi bi-qr-code-scan"></i> scan icon → scan this QR code.
</p>
<div class="d-flex align-items-center justify-content-center gap-2 mt-2">
<div class="spinner-border spinner-border-sm text-info" role="status" id="qr-spinner">
<span class="visually-hidden">Waiting…</span>
</div>
<span class="text-muted small" id="qr-poll-msg">Waiting for scan…</span>
</div>
<button id="btn-cancel-qr" class="btn btn-outline-secondary btn-sm mt-3">Cancel</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- QR code renderer (MIT, CDN) -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1/build/qrcode.min.js"></script>
<script>
(function() {
const BOARD_ID = {{ board.id }};
const QR_URL = "{{ url_for('tuya.generate_qr', board_id=board.id) }}";
const POLL_URL = "{{ url_for('tuya.poll_login', board_id=board.id) }}";
const SAVE_URL = "{{ url_for('tuya.save_auth', board_id=board.id) }}";
const GW_URL = "{{ url_for('tuya.gateway', board_id=board.id) }}";
let pollTimer = null;
let userCode = null;
let qrToken = null;
const btnStart = document.getElementById('btn-start-qr');
const btnCancel = document.getElementById('btn-cancel-qr');
const qrArea = document.getElementById('qr-area');
const qrCanvas = document.getElementById('qr-canvas');
const spinner = document.getElementById('qr-spinner');
const pollMsg = document.getElementById('qr-poll-msg');
const statusDiv = document.getElementById('linked-status');
const inputCode = document.getElementById('input-user-code');
btnStart.addEventListener('click', startQr);
btnCancel.addEventListener('click', cancelQr);
function startQr() {
const code = inputCode.value.trim();
if (!code) {
inputCode.focus();
inputCode.classList.add('is-invalid');
return;
}
inputCode.classList.remove('is-invalid');
btnStart.disabled = true;
btnStart.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Generating…';
fetch(QR_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_code: code }),
})
.then(r => r.json())
.then(function(data) {
if (!data.ok) { showError(data.error); btnStart.disabled = false; btnStart.innerHTML = '<i class="bi bi-qr-code me-1"></i>Generate QR Code'; return; }
userCode = data.user_code;
qrToken = data.qr_token;
// Render QR code into canvas div
qrCanvas.innerHTML = '';
QRCode.toCanvas(document.createElement('canvas'), data.qr_data, { width: 220 }, function(err, canvas) {
if (err) { showError('QR render failed: ' + err); return; }
qrCanvas.appendChild(canvas);
});
qrArea.classList.remove('d-none');
btnStart.classList.add('d-none');
inputCode.disabled = true;
pollMsg.textContent = 'Waiting for scan…';
spinner.classList.remove('d-none');
// Start polling every 3 s
pollTimer = setInterval(pollLogin, 3000);
})
.catch(function(err) { showError(err); btnStart.disabled = false; btnStart.innerHTML = '<i class="bi bi-qr-code me-1"></i>Generate QR Code'; });
}
function pollLogin() {
const url = POLL_URL + '?token=' + encodeURIComponent(qrToken) + '&user_code=' + encodeURIComponent(userCode);
fetch(url)
.then(r => r.json())
.then(function(data) {
if (data.ok) {
clearInterval(pollTimer);
pollMsg.textContent = '✓ Scanned! Saving…';
spinner.classList.add('d-none');
saveTokens(data.info);
} else if (data.error) {
clearInterval(pollTimer);
pollMsg.textContent = 'Error: ' + data.error;
spinner.classList.add('d-none');
}
// else data.pending: keep waiting
})
.catch(function() { /* network hiccup keep polling */ });
}
function saveTokens(info) {
fetch(SAVE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ info: info, user_code: userCode }),
})
.then(r => r.json())
.then(function(data) {
if (data.ok) {
pollMsg.textContent = '✓ Account linked successfully! Redirecting…';
setTimeout(function() { window.location.href = data.redirect || GW_URL; }, 1500);
} else {
showError(data.error || 'Save failed');
}
})
.catch(function(err) { showError(err); });
}
function cancelQr() {
clearInterval(pollTimer);
qrArea.classList.add('d-none');
btnStart.classList.remove('d-none');
btnStart.disabled = false;
btnStart.innerHTML = '<i class="bi bi-qr-code me-1"></i>Generate QR Code';
qrCanvas.innerHTML = '';
inputCode.disabled = false;
}
function showError(msg) {
const el = document.createElement('div');
el.className = 'alert alert-danger py-2 mt-2';
el.textContent = msg;
qrCanvas.after(el);
}
})();
</script>
{% endblock %}