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
This commit is contained in:
@@ -257,11 +257,15 @@
|
||||
// relay type badge
|
||||
const typeText = opts.entityType === "relay" ? "relay"
|
||||
: opts.entityType === "sonoff" ? "sonoff"
|
||||
: opts.entityType === "tuya" ? "tuya"
|
||||
: "input";
|
||||
const badge = new Konva.Text({
|
||||
x: 36, y: 38,
|
||||
text: typeText, fontSize: 9, fontFamily: "system-ui, sans-serif",
|
||||
fill: opts.entityType === "relay" ? "#f0883e" : opts.entityType === "sonoff" ? "#58a6ff" : "#3fb950",
|
||||
fill: opts.entityType === "relay" ? "#f0883e"
|
||||
: opts.entityType === "sonoff" ? "#58a6ff"
|
||||
: opts.entityType === "tuya" ? "#20c997"
|
||||
: "#3fb950",
|
||||
name: "device-type",
|
||||
});
|
||||
|
||||
@@ -310,6 +314,8 @@
|
||||
.replace("{relayNum}", opts.entityNum);
|
||||
} else if (opts.entityType === "sonoff") {
|
||||
url = `/sonoff/${opts.boardId}/device/${encodeURIComponent(opts.deviceId)}/ch/${opts.channel}/toggle`;
|
||||
} else if (opts.entityType === "tuya") {
|
||||
url = `/tuya/${opts.boardId}/device/${encodeURIComponent(opts.deviceId)}/dp/${encodeURIComponent(opts.channel)}/toggle`;
|
||||
} else {
|
||||
return; // inputs are read-only
|
||||
}
|
||||
@@ -399,6 +405,14 @@
|
||||
isOn = entityInfo.isOn;
|
||||
onColor = entityInfo.kind === "light" ? "warning" : "success";
|
||||
offColor = "secondary";
|
||||
} else if (d.entityType === "tuya") {
|
||||
// Match by deviceId + channel (dp_code) stored in tuya_channels
|
||||
entityInfo = board.tuya_channels &&
|
||||
board.tuya_channels.find(tc => tc.deviceId === d.deviceId && tc.channel === d.channel);
|
||||
if (!entityInfo) return;
|
||||
isOn = entityInfo.isOn;
|
||||
onColor = entityInfo.kind === "light" ? "warning" : "success";
|
||||
offColor = "secondary";
|
||||
} else if (d.entityType === "relay") {
|
||||
entityInfo = board.relays.find(r => r.num === d.entityNum);
|
||||
if (!entityInfo) return;
|
||||
@@ -427,7 +441,7 @@
|
||||
boardName: board.name,
|
||||
icon: entityInfo.icon,
|
||||
stateColor: colorForState(isOn, onColor, offColor),
|
||||
isRelay: d.entityType === "relay" || d.entityType === "sonoff",
|
||||
isRelay: d.entityType === "relay" || d.entityType === "sonoff" || d.entityType === "tuya",
|
||||
});
|
||||
deviceLayer.add(group);
|
||||
});
|
||||
@@ -837,6 +851,13 @@
|
||||
hasAny = true;
|
||||
});
|
||||
}
|
||||
// ── Tuya sub-devices ─────────────────────────────────────────────
|
||||
if (board.tuya_channels && board.tuya_channels.length) {
|
||||
board.tuya_channels.forEach(tc => {
|
||||
grp.appendChild(makeTuyaChip(board, tc));
|
||||
hasAny = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (hasAny) list.appendChild(grp);
|
||||
});
|
||||
@@ -928,6 +949,46 @@
|
||||
return chip;
|
||||
}
|
||||
|
||||
function makeTuyaChip(board, tc) {
|
||||
const chip = document.createElement("div");
|
||||
chip.className = "lb-device-chip";
|
||||
chip.draggable = true;
|
||||
chip.dataset.boardId = board.id;
|
||||
chip.dataset.entityType = "tuya";
|
||||
chip.dataset.deviceId = tc.deviceId;
|
||||
chip.dataset.channel = tc.channel; // dp_code string
|
||||
chip.dataset.name = tc.name;
|
||||
chip.dataset.boardName = board.name;
|
||||
chip.dataset.isOn = tc.isOn;
|
||||
|
||||
const onColor = tc.kind === "light" ? "warning" : "success";
|
||||
const dotColor = colorForState(tc.isOn, onColor, "secondary");
|
||||
|
||||
chip.innerHTML = `
|
||||
<span class="chip-dot" style="background:${dotColor}"></span>
|
||||
<i class="bi ${escHtml(tc.icon)}" style="font-size:13px"></i>
|
||||
<span class="text-truncate">${escHtml(tc.name)}</span>
|
||||
<span class="ms-auto text-secondary" style="font-size:.7rem">${escHtml(tc.kind || 'switch')}</span>`;
|
||||
|
||||
chip.addEventListener("dragstart", (e) => {
|
||||
e.dataTransfer.effectAllowed = "copy";
|
||||
e.dataTransfer.setData("application/json", JSON.stringify({
|
||||
boardId: parseInt(board.id),
|
||||
entityType: "tuya",
|
||||
entityNum: null,
|
||||
deviceId: tc.deviceId,
|
||||
channel: tc.channel,
|
||||
name: tc.name,
|
||||
boardName: board.name,
|
||||
onColor,
|
||||
offColor: "secondary",
|
||||
isOn: tc.isOn,
|
||||
}));
|
||||
});
|
||||
|
||||
return chip;
|
||||
}
|
||||
|
||||
// ── canvas drop target ────────────────────────────────────────────────────
|
||||
const ghost = document.getElementById("lb-drag-ghost");
|
||||
|
||||
@@ -979,7 +1040,7 @@
|
||||
name: dragged.name,
|
||||
boardName: dragged.boardName,
|
||||
stateColor,
|
||||
isRelay: dragged.entityType === "relay" || dragged.entityType === "sonoff",
|
||||
isRelay: dragged.entityType === "relay" || dragged.entityType === "sonoff" || dragged.entityType === "tuya",
|
||||
});
|
||||
|
||||
deviceLayer.add(group);
|
||||
@@ -1020,6 +1081,25 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Tuya devices emit a "tuya_update" event
|
||||
socket.on("tuya_update", (data) => {
|
||||
const bid = data.board_id;
|
||||
|
||||
deviceLayer.find(".device-group").forEach(group => {
|
||||
if (group.attrs.boardId !== bid) return;
|
||||
if (group.attrs.entityType !== "tuya") return;
|
||||
if (group.attrs.deviceId !== data.device_id) return;
|
||||
if (group.attrs.channel !== data.dp_code) return;
|
||||
|
||||
const board = CFG.boards.find(b => b.id === bid);
|
||||
const tc = board && board.tuya_channels &&
|
||||
board.tuya_channels.find(t =>
|
||||
t.deviceId === data.device_id && t.channel === data.dp_code);
|
||||
const onColor = tc ? (tc.kind === "light" ? "warning" : "success") : "success";
|
||||
updateDeviceState(group, data.state, onColor, "secondary");
|
||||
});
|
||||
});
|
||||
|
||||
// Sonoff sub-devices emit a separate "sonoff_update" event
|
||||
socket.on("sonoff_update", (data) => {
|
||||
const bid = data.board_id;
|
||||
|
||||
Reference in New Issue
Block a user