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

215 lines
8.8 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 }} Tuya Cloud Gateway{% endblock %}
{% block content %}
<div class="container-fluid py-3">
{# ── Page header ──────────────────────────────────────────────────── #}
<div class="d-flex align-items-center justify-content-between mb-4 gap-2 flex-wrap">
<div class="d-flex align-items-center gap-3">
<a href="{{ url_for('boards.list_boards') }}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i>
</a>
<div>
<h4 class="mb-0 fw-bold">
<i class="bi bi-cloud-fill text-info me-2"></i>{{ board.name }}
</h4>
<small class="text-muted">Tuya Cloud Gateway
{% if board.is_online %}
<span class="badge bg-success ms-2">Online</span>
{% else %}
<span class="badge bg-secondary ms-2">Offline</span>
{% endif %}
</small>
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
{% if current_user.is_admin() %}
<form method="post" action="{{ url_for('tuya.sync_devices', board_id=board.id) }}">
<button class="btn btn-primary btn-sm">
<i class="bi bi-arrow-repeat me-1"></i>Sync Devices
</button>
</form>
<a href="{{ url_for('tuya.auth_settings', board_id=board.id) }}" class="btn btn-outline-light btn-sm">
<i class="bi bi-key me-1"></i>Tuya Settings
</a>
{% endif %}
</div>
</div>
{# ── Flash messages ───────────────────────────────────────────────── #}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show py-2" role="alert">
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{# ── No token warning ─────────────────────────────────────────────── #}
{% if not has_token %}
<div class="alert alert-warning d-flex align-items-center gap-3" role="alert">
<i class="bi bi-exclamation-triangle-fill fs-4"></i>
<div>
<strong>Tuya account not linked.</strong>
<a href="{{ url_for('tuya.auth_settings', board_id=board.id) }}" class="alert-link">
Scan a QR code with the Smart Life app
</a> to discover and control devices.
</div>
</div>
{% endif %}
{# ── Device grid ──────────────────────────────────────────────────── #}
{% if devices %}
<div class="row g-3">
{% for dev in devices %}
{% set icon_cls = {
'switch': 'bi-toggles',
'light': 'bi-lightbulb-fill',
'fan': 'bi-fan',
'sensor': 'bi-thermometer-half',
'cover': 'bi-door-open',
}.get(dev.kind, 'bi-plug') %}
<div class="col-12 col-md-6 col-xl-4" id="tuya-card-{{ dev.device_id }}">
<div class="card bg-dark border-secondary h-100">
<div class="card-header d-flex align-items-center justify-content-between py-2">
<div class="d-flex align-items-center gap-2">
<i class="bi {{ icon_cls }} text-info"></i>
<span class="fw-semibold text-truncate" style="max-width:160px"
title="{{ dev.name }}">{{ dev.name }}</span>
</div>
<div class="d-flex align-items-center gap-2">
{% if dev.is_online %}
<span class="badge bg-success">Online</span>
{% else %}
<span class="badge bg-secondary">Offline</span>
{% endif %}
<a href="{{ url_for('tuya.device_detail', board_id=board.id, device_id=dev.device_id) }}"
class="btn btn-outline-secondary btn-sm px-2 py-0">
<i class="bi bi-sliders"></i>
</a>
</div>
</div>
<div class="card-body py-2">
<div class="text-muted small mb-2">
{{ dev.kind | capitalize }}
{% if dev.product_name %}
{{ dev.product_name }}
{% endif %}
</div>
{# ── Switch / light / fan toggles ─────────────────────────── #}
{% if dev.kind in ('switch', 'light', 'fan') and dev.switch_dps %}
<div class="d-flex flex-wrap gap-2">
{% for dp in dev.switch_dps %}
{% set ch_idx = loop.index0 %}
{% set ch_on = dev.status.get(dp, False) %}
<div class="d-flex align-items-center gap-2 border border-secondary rounded px-2 py-1"
id="tuya-dp-{{ dev.device_id }}-{{ dp }}">
<span class="small text-muted">
{% if dev.num_channels == 1 %}Power{% else %}CH{{ loop.index }}{% endif %}
</span>
<button
class="btn btn-sm px-2 py-0 tuya-dp-btn {{ 'btn-info' if ch_on else 'btn-outline-info' }}"
data-url="{{ url_for('tuya.toggle_dp', board_id=board.id, device_id=dev.device_id, dp_code=dp) }}"
data-device="{{ dev.device_id }}" data-dp="{{ dp }}"
title="Toggle {{ dp }}"
>{{ 'ON' if ch_on else 'OFF' }}</button>
</div>
{% endfor %}
</div>
{# ── Sensor show formatted live readings ────────────────── #}
{% elif dev.kind == 'sensor' %}
{# Hide config/threshold keys that clutter the card #}
{% set _skip_suffixes = ['_set', '_alarm', '_convert', '_mode'] %}
<dl class="row mb-0 small">
{% for k, v in dev.status.items() %}
{% set _hide = namespace(val=false) %}
{% for sfx in _skip_suffixes %}
{% if k.endswith(sfx) %}{% set _hide.val = true %}{% endif %}
{% endfor %}
{% if not _hide.val %}
{% set fv = v | tuya_dp(k, dev.status) %}
<dt class="col-6 text-muted text-truncate" title="{{ k }}">{{ k }}</dt>
<dd class="col-6 mb-1">
{% if fv is sameas true %}<span class="badge bg-success">on</span>
{% elif fv is sameas false %}<span class="badge bg-secondary">off</span>
{% else %}{{ fv }}{% endif %}
</dd>
{% endif %}
{% endfor %}
</dl>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center text-muted py-5">
<i class="bi bi-cloud-slash fs-1 d-block mb-3 opacity-50"></i>
<p>No Tuya devices found.</p>
{% if has_token %}
<form method="post" action="{{ url_for('tuya.sync_devices', board_id=board.id) }}" class="d-inline">
<button class="btn btn-outline-primary btn-sm">
<i class="bi bi-arrow-repeat me-1"></i>Sync now
</button>
</form>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
/* ── AJAX toggles ─────────────────────────────────────────────────────────── */
document.querySelectorAll('.tuya-dp-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
const url = btn.dataset.url;
const device = btn.dataset.device;
const dp = btn.dataset.dp;
btn.disabled = true;
fetch(url, {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json'},
})
.then(r => r.json())
.then(function(data) {
updateDpButton(device, dp, data.state);
})
.catch(function() { btn.disabled = false; });
});
});
function updateDpButton(deviceId, dp, state) {
const wrap = document.getElementById('tuya-dp-' + deviceId + '-' + dp);
if (!wrap) return;
const btn = wrap.querySelector('.tuya-dp-btn');
if (!btn) return;
btn.disabled = false;
if (state) {
btn.textContent = 'ON';
btn.classList.replace('btn-outline-info', 'btn-info');
} else {
btn.textContent = 'OFF';
btn.classList.replace('btn-info', 'btn-outline-info');
}
}
/* ── Socket.IO live updates ───────────────────────────────────────────────── */
if (typeof io !== 'undefined') {
const socket = io();
socket.on('tuya_update', function(data) {
if (data.board_id !== {{ board.id }}) return;
updateDpButton(data.device_id, data.dp_code, data.state);
});
}
</script>
{% endblock %}