import fs from 'fs';
import http from 'http';
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,
now: Date.now()
};
}
const statusPageHTML = `
Water Timer
💧 Water Timer
Button
Checking…
Short press: run timer · Long press: start learn, short press to stop
Valve
Checking…
⚠️ Connect according to flow direction mark
Loading…
`;
// Prevent duplicate listeners across hot reloads
if (global.__waterStatusServer) {
try { global.__waterStatusServer.close(); } catch (e) { }
}
const srv = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const jsonHeaders = {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
'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 === '/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 === '/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;
}
// ── POST command endpoints ──
if (req.method === 'POST' && url.pathname.startsWith('/api/')) {
let body = '';
req.on('data', c => body += c);
req.on('end', async () => {
const sendRPC = global.__waterBotRPC;
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; }
state.countMode = false;
await setLight(sendRPC, WATER_BUTTON_MAC, false, 0);
const duration = state.storedDuration;
state.timerStartedAt = Date.now();
state.timerEndsAt = Date.now() + duration;
state.timer = setTimeout(async () => {
try {
const rpc = global.__waterBotRPC;
if (rpc) await setLight(rpc, WATER_BUTTON_MAC, true, 95);
state.timer = null;
state.timerStartedAt = null;
state.timerEndsAt = null;
} catch (err) {
console.error('[WaterStatus] Timer callback error:', err);
}
}, duration);
res.writeHead(200, jsonHeaders);
res.end(JSON.stringify({ ok: true, action: 'run', duration }));
} else if (url.pathname === '/api/open') {
if (state.timer) { clearTimeout(state.timer); state.timer = null; }
state.countMode = false;
state.timerStartedAt = null;
state.timerEndsAt = null;
await setLight(sendRPC, WATER_BUTTON_MAC, false, 0);
res.writeHead(200, jsonHeaders);
res.end(JSON.stringify({ ok: true, action: 'open' }));
} else if (url.pathname === '/api/close') {
if (state.timer) { clearTimeout(state.timer); state.timer = null; }
state.countMode = false;
state.timerStartedAt = null;
state.timerEndsAt = null;
await setLight(sendRPC, WATER_BUTTON_MAC, true, 95);
res.writeHead(200, jsonHeaders);
res.end(JSON.stringify({ ok: true, action: 'close' }));
} else if (url.pathname === '/api/duration') {
const data = JSON.parse(body || '{}');
const ms = Math.round((data.seconds || 0) * 1000);
if (ms <= 0 || ms > 300000) {
res.writeHead(400, jsonHeaders);
res.end(JSON.stringify({ ok: false, error: 'Duration must be 1-300s' }));
return;
}
state.storedDuration = ms;
persistedState[WATER_BUTTON_MAC] = { storedDuration: ms };
saveState(persistedState);
res.writeHead(200, jsonHeaders);
res.end(JSON.stringify({ ok: true, action: 'duration', ms }));
} else {
res.writeHead(404, jsonHeaders);
res.end(JSON.stringify({ ok: false, error: 'Unknown endpoint' }));
}
} catch (err) {
console.error('[WaterStatus] API error:', err);
res.writeHead(500, jsonHeaders);
res.end(JSON.stringify({ ok: false, error: err.message }));
}
});
return;
}
// Serve HTML status page
res.writeHead(200, {
'Content-Type': 'text/html',
'Cache-Control': 'no-cache'
});
res.end(statusPageHTML);
});
srv.listen(STATUS_PORT, () => {
console.log(`[WaterStatus] Status page at http://localhost:${STATUS_PORT}`);
});
global.__waterStatusServer = srv;
}