Files
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

166 lines
6.6 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 %}{{ device.name }} Tuya Device{% endblock %}
{% block content %}
<div class="container py-4" style="max-width:640px">
{# ── Breadcrumb ──────────────────────────────────────────────────── #}
<nav aria-label="breadcrumb" class="small mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('boards.list_boards') }}">Boards</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('tuya.gateway', board_id=board.id) }}">{{ board.name }}</a></li>
<li class="breadcrumb-item active">{{ device.name }}</li>
</ol>
</nav>
{# ── Device card ─────────────────────────────────────────────────── #}
{% set icon_cls = {
'switch': 'bi-toggles',
'light': 'bi-lightbulb-fill',
'fan': 'bi-fan',
'sensor': 'bi-thermometer-half',
'cover': 'bi-door-open',
}.get(device.kind, 'bi-plug') %}
<div class="card bg-dark border-secondary mb-4">
<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 }} fs-5 text-info"></i>
<span class="fw-bold">{{ device.name }}</span>
</div>
{% if device.is_online %}
<span class="badge bg-success">Online</span>
{% else %}
<span class="badge bg-secondary">Offline</span>
{% endif %}
</div>
<div class="card-body">
<dl class="row small mb-0">
<dt class="col-5 text-muted">Device ID</dt>
<dd class="col-7 font-monospace text-break">{{ device.device_id }}</dd>
<dt class="col-5 text-muted">Category</dt>
<dd class="col-7">{{ device.category }} ({{ device.kind }})</dd>
{% if device.product_name %}
<dt class="col-5 text-muted">Product</dt>
<dd class="col-7">{{ device.product_name }}</dd>
{% endif %}
{% if device.last_seen %}
<dt class="col-5 text-muted">Last seen</dt>
<dd class="col-7">{{ device.last_seen.strftime('%Y-%m-%d %H:%M') }}</dd>
{% endif %}
</dl>
</div>
</div>
{# ── Controls ────────────────────────────────────────────────────── #}
{% if device.switch_dps %}
<div class="card bg-dark border-secondary mb-4">
<div class="card-header py-2"><strong>Controls</strong></div>
<div class="card-body d-flex flex-wrap gap-3">
{% for dp in device.switch_dps %}
{% set ch_on = device.status.get(dp, False) %}
<div class="d-flex align-items-center gap-2 border border-secondary rounded px-3 py-2"
id="ctrl-dp-{{ dp }}">
<span class="text-muted small">
{% if device.num_channels == 1 %}Power{% else %}CH{{ loop.index }}{% endif %}
<span class="font-monospace opacity-50">({{ dp }})</span>
</span>
<button
class="btn btn-sm px-3 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=device.device_id, dp_code=dp) }}"
data-dp="{{ dp }}"
>{{ 'ON' if ch_on else 'OFF' }}</button>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{# ── Full status ─────────────────────────────────────────────────── #}
{% if device.status %}
<div class="card bg-dark border-secondary mb-4">
<div class="card-header py-2 d-flex align-items-center justify-content-between">
<strong>Full Status</strong>
<small class="text-muted">All data points</small>
</div>
<div class="card-body p-0">
<table class="table table-dark table-sm table-striped mb-0">
<thead><tr><th>DP Code</th><th>Value</th></tr></thead>
<tbody>
{% for k, v in device.status.items() %}
{% set fv = v | tuya_dp(k, device.status) %}
<tr>
<td class="font-monospace text-muted">{{ k }}</td>
<td>
{% if fv is sameas true %}
<span class="badge bg-success">true</span>
{% elif fv is sameas false %}
<span class="badge bg-secondary">false</span>
{% else %}
{{ fv }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{# ── Rename ──────────────────────────────────────────────────────── #}
{% if current_user.is_admin() %}
<div class="card bg-dark border-secondary">
<div class="card-header py-2"><strong>Rename Device</strong></div>
<div class="card-body">
<form method="post" action="{{ url_for('tuya.rename_device', board_id=board.id, device_id=device.device_id) }}"
class="d-flex gap-2">
<input type="text" name="name" class="form-control form-control-sm"
value="{{ device.name }}" required style="max-width:300px">
<button type="submit" class="btn btn-outline-primary btn-sm">
<i class="bi bi-pencil me-1"></i>Rename
</button>
</form>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
document.querySelectorAll('.tuya-dp-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
const url = btn.dataset.url;
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) {
const wrap = document.getElementById('ctrl-dp-' + dp);
if (!wrap) return;
const b = wrap.querySelector('.tuya-dp-btn');
if (!b) return;
b.disabled = false;
if (data.state) {
b.textContent = 'ON';
b.classList.replace('btn-outline-info', 'btn-info');
} else {
b.textContent = 'OFF';
b.classList.replace('btn-info', 'btn-outline-info');
}
})
.catch(function() { btn.disabled = false; });
});
});
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}{% for cat, msg in messages %}{# consumed by parent template #}{% endfor %}{% endif %}
{% endwith %}
</script>
{% endblock %}