feat: Introduce a Blockly-based visual rule editor with AST generation and add new assets for water button status.
This commit is contained in:
@@ -6,16 +6,45 @@ const STATUS_PORT = 8082;
|
||||
export function startStatusServer({
|
||||
getState,
|
||||
WATER_BUTTON_MAC,
|
||||
REMOTE_SWITCH_MAC,
|
||||
setLight,
|
||||
persistedState,
|
||||
saveState
|
||||
}) {
|
||||
async function getButtonOnline() {
|
||||
try {
|
||||
const ctxGetState = global.__waterBotGetState;
|
||||
if (!ctxGetState) return null;
|
||||
const online = await ctxGetState(WATER_BUTTON_MAC, 'system', 'online');
|
||||
// DB stores booleans as strings 'true'/'false'
|
||||
if (online === true || online === 'true') return true;
|
||||
if (online === false || online === 'false') return false;
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getValveOnline() {
|
||||
try {
|
||||
const ctxGetState = global.__waterBotGetState;
|
||||
if (!ctxGetState) return null;
|
||||
const online = await ctxGetState(REMOTE_SWITCH_MAC, 'system', 'online');
|
||||
if (online === true || online === 'true') return true;
|
||||
if (online === false || online === 'false') return false;
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusJSON() {
|
||||
const state = getState(WATER_BUTTON_MAC);
|
||||
return {
|
||||
valveOpen: state.timer !== null || (!state.countMode && state.timer === null),
|
||||
timerActive: state.timer !== null,
|
||||
countMode: state.countMode,
|
||||
countStart: state.countStart || null,
|
||||
storedDuration: state.storedDuration,
|
||||
timerStartedAt: state.timerStartedAt,
|
||||
timerEndsAt: state.timerEndsAt,
|
||||
@@ -35,6 +64,31 @@ export function startStatusServer({
|
||||
<div class="container">
|
||||
<h1>💧 Water Timer</h1>
|
||||
|
||||
<div class="devices-row">
|
||||
<div class="device-status">
|
||||
<div class="device-icon-wrap">
|
||||
<img src="/taster.png" class="device-icon" alt="Button">
|
||||
<span class="device-dot" id="device-dot"></span>
|
||||
</div>
|
||||
<div class="device-info">
|
||||
<span class="device-name">Button</span>
|
||||
<span class="device-state" id="device-state">Checking…</span>
|
||||
<span class="device-desc">Short press: run timer · Long press: start learn, short press to stop</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-status">
|
||||
<div class="device-icon-wrap">
|
||||
<img src="/valve.png" class="device-icon" alt="Valve">
|
||||
<span class="device-dot" id="valve-dot"></span>
|
||||
</div>
|
||||
<div class="device-info">
|
||||
<span class="device-name">Valve</span>
|
||||
<span class="device-state" id="valve-state">Checking…</span>
|
||||
<span class="device-desc">⚠️ Connect according to flow direction mark</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ring-wrapper">
|
||||
<div class="glow" id="glow"></div>
|
||||
<svg class="ring-svg" viewBox="0 0 120 120">
|
||||
@@ -66,6 +120,7 @@ export function startStatusServer({
|
||||
<span class="dur-unit">sec</span>
|
||||
</div>
|
||||
<button class="btn primary" id="btn-run" onclick="doRun()">▶ Run</button>
|
||||
<button class="btn warning" id="btn-learn" onclick="doLearn()">📐 Learn</button>
|
||||
</div>
|
||||
<button class="btn success" id="btn-open" onclick="doOpen()">🔓 Open</button>
|
||||
<button class="btn danger" id="btn-close" onclick="doClose()">🔒 Close</button>
|
||||
@@ -81,7 +136,7 @@ export function startStatusServer({
|
||||
try { global.__waterStatusServer.close(); } catch (e) { }
|
||||
}
|
||||
|
||||
const srv = http.createServer((req, res) => {
|
||||
const srv = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
|
||||
const jsonHeaders = {
|
||||
@@ -90,19 +145,45 @@ export function startStatusServer({
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
};
|
||||
|
||||
if (url.pathname === '/style.css') {
|
||||
res.writeHead(200, { 'Content-Type': 'text/css' });
|
||||
res.end(fs.readFileSync(new URL('./public/waterButtonStatus/style.css', import.meta.url)));
|
||||
return;
|
||||
}
|
||||
if (url.pathname === '/style.css') {
|
||||
res.writeHead(200, { 'Content-Type': 'text/css' });
|
||||
res.end(fs.readFileSync(new URL('./public/waterButtonStatus/style.css', import.meta.url)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/script.js') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
||||
res.end(fs.readFileSync(new URL('./public/waterButtonStatus/script.js', import.meta.url)));
|
||||
return;
|
||||
}
|
||||
if (url.pathname === '/script.js') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
||||
res.end(fs.readFileSync(new URL('./public/waterButtonStatus/script.js', import.meta.url)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/status') {
|
||||
if (url.pathname === '/taster.png') {
|
||||
res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=86400' });
|
||||
res.end(fs.readFileSync(new URL('./public/waterButtonStatus/taster.png', import.meta.url)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/button-online') {
|
||||
const online = await getButtonOnline();
|
||||
res.writeHead(200, jsonHeaders);
|
||||
res.end(JSON.stringify({ online }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/valve.png') {
|
||||
res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=86400' });
|
||||
res.end(fs.readFileSync(new URL('./public/waterButtonStatus/valve.png', import.meta.url)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/valve-online') {
|
||||
const online = await getValveOnline();
|
||||
res.writeHead(200, jsonHeaders);
|
||||
res.end(JSON.stringify({ online }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/status') {
|
||||
res.writeHead(200, jsonHeaders);
|
||||
res.end(JSON.stringify(getStatusJSON()));
|
||||
return;
|
||||
@@ -114,17 +195,55 @@ export function startStatusServer({
|
||||
req.on('data', c => body += c);
|
||||
req.on('end', async () => {
|
||||
const sendRPC = global.__waterBotRPC;
|
||||
|
||||
// Wait, inside the route we need `state` which uses `getState(WATER_BUTTON_MAC)`
|
||||
// The original code did: const state = getState(WATER_BUTTON_MAC);
|
||||
const state = getState(WATER_BUTTON_MAC);
|
||||
|
||||
// ── Learn / Stop-learn: work regardless of device connection ──
|
||||
if (url.pathname === '/api/learn') {
|
||||
if (state.timer) { clearTimeout(state.timer); state.timer = null; }
|
||||
state.timerStartedAt = null;
|
||||
state.timerEndsAt = null;
|
||||
state.countMode = true;
|
||||
state.countStart = Date.now();
|
||||
if (sendRPC) {
|
||||
try { await setLight(sendRPC, WATER_BUTTON_MAC, false, 0); } catch (e) {
|
||||
console.warn('[WaterStatus] learn: setLight failed:', e.message);
|
||||
}
|
||||
}
|
||||
res.writeHead(200, jsonHeaders);
|
||||
res.end(JSON.stringify({ ok: true, action: 'learn' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/stop-learn') {
|
||||
if (!state.countMode) {
|
||||
res.writeHead(200, jsonHeaders);
|
||||
res.end(JSON.stringify({ ok: true, action: 'stop-learn', skipped: true }));
|
||||
return;
|
||||
}
|
||||
const elapsed = Date.now() - (state.countStart || Date.now());
|
||||
state.countMode = false;
|
||||
state.countStart = 0;
|
||||
state.storedDuration = elapsed;
|
||||
persistedState[WATER_BUTTON_MAC] = { storedDuration: elapsed };
|
||||
saveState(persistedState);
|
||||
if (sendRPC) {
|
||||
try { await setLight(sendRPC, WATER_BUTTON_MAC, true, 95); } catch (e) {
|
||||
console.warn('[WaterStatus] stop-learn: setLight failed:', e.message);
|
||||
}
|
||||
}
|
||||
res.writeHead(200, jsonHeaders);
|
||||
res.end(JSON.stringify({ ok: true, action: 'stop-learn', storedDuration: elapsed }));
|
||||
return;
|
||||
}
|
||||
|
||||
// ── All other commands require a live device connection ──
|
||||
if (!sendRPC) {
|
||||
res.writeHead(503, jsonHeaders);
|
||||
res.end(JSON.stringify({ ok: false, error: 'Not ready — no device connection' }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
if (url.pathname === '/api/run') {
|
||||
if (state.timer) { clearTimeout(state.timer); state.timer = null; }
|
||||
|
||||
Reference in New Issue
Block a user