/** * Timer light rule * * - btn_down: Light goes off * - long_push: Flash to confirm, enter "count mode" - counts seconds until next btn_down, then light goes on * - Normal btn_down (no long_push): Light goes off, then turns back on after the stored count elapsed * * Also syncs remote switch CC8DA243B0A0 inversely (light off = switch on, light on = switch off) * * Telegram bot (remote control): * /open — Open valve * /close — Close valve * /run — Close valve then reopen after stored duration * /duration — Get or set timer duration (e.g. 30s, 2m, 10000) * /status — Show current status */ import fs from 'fs'; import path from 'path'; import { startStatusServer } from '../waterButtonStatusServer.js'; import { startTelegramBot } from '../waterButtonTelegramBot.js'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); const STATE_FILE = path.join(__dirname, 'timer_state.json'); const WATER_BUTTON_MAC = '08A6F773510C'; const REMOTE_SWITCH_MAC = 'CC8DA243B0A0'; // Telegram // sendRPC / getState persisted across hot reloads via globals (populated on first ctx event) if (!global.__waterBotRPC) global.__waterBotRPC = null; if (!global.__waterBotGetState) global.__waterBotGetState = null; // ── Device helpers ──────────────────────────────────────────────────────────── async function setLight(sendRPC, mac, on, brightness) { await sendRPC(mac, 'Light.Set', { id: 0, on, brightness }); // Remote switch is inverse: light off → switch on, light on → switch off await sendRPC(REMOTE_SWITCH_MAC, 'Switch.Set', { id: 0, on: !on }); } // ── Persistence ─────────────────────────────────────────────────────────────── function loadPersistedState() { try { if (fs.existsSync(STATE_FILE)) { return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); } } catch (err) { console.error('Error loading state:', err); } return {}; } function saveState(state) { try { const toSave = {}; for (const [mac, data] of Object.entries(state)) { toSave[mac] = { storedDuration: data.storedDuration }; } fs.writeFileSync(STATE_FILE, JSON.stringify(toSave, null, 2)); } catch (err) { console.error('Error saving state:', err); } } // ── In-memory device state ──────────────────────────────────────────────────── const persistedState = loadPersistedState(); const deviceState = new Map(); function getState(mac) { if (!deviceState.has(mac)) { const persisted = persistedState[mac] || {}; deviceState.set(mac, { countMode: false, countStart: 0, storedDuration: persisted.storedDuration || 5000, timer: null, timerStartedAt: null, timerEndsAt: null }); } return deviceState.get(mac); } startTelegramBot({ getState, WATER_BUTTON_MAC, REMOTE_SWITCH_MAC, setLight, persistedState, saveState }); // ── Rule export ─────────────────────────────────────────────────────────────── export default { getStatus() { const state = getState(WATER_BUTTON_MAC); return { storedDuration: state.storedDuration, countMode: state.countMode, timerActive: state.timer !== null }; }, setConfig(key, value) { if (key === 'storedDuration') { const duration = parseInt(value, 10); if (duration > 0 && duration <= 300000) { const state = getState(WATER_BUTTON_MAC); state.storedDuration = duration; persistedState[WATER_BUTTON_MAC] = { storedDuration: duration }; saveState(persistedState); console.log(`[Rule] storedDuration set to ${duration}ms`); return true; } } return false; }, async run(ctx) { // Make sendRPC and getState available to the Telegram bot handlers global.__waterBotRPC = ctx.sendRPC; global.__waterBotGetState = ctx.getState; // ── Online / offline events ────────────────────────────────────────── // Water button comes online — turn light on if remote switch is also online if (ctx.trigger.mac === WATER_BUTTON_MAC && ctx.trigger.field === 'online' && ctx.trigger.event === true) { const remoteSwitchConnected = await ctx.getState(REMOTE_SWITCH_MAC, 'system', 'online'); if (remoteSwitchConnected === true) { ctx.log('Water button connected - remote switch online, turning light on'); for (let i = 0; i < 2; i++) { await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: true, brightness: 95 }); await sleep(200); await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: false, brightness: 0 }); await sleep(200); } await setLight(ctx.sendRPC, ctx.trigger.mac, true, 95); } else { ctx.log('Water button connected - remote switch offline, keeping light off'); await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: false, brightness: 0 }); } return; } // Remote switch comes online if (ctx.trigger.mac === REMOTE_SWITCH_MAC && ctx.trigger.field === 'online' && ctx.trigger.event === true) { ctx.log('Remote switch connected - turning switch off, flashing light, then turning light on'); await ctx.sendRPC(REMOTE_SWITCH_MAC, 'Switch.Set', { id: 0, on: false }); for (let i = 0; i < 2; i++) { await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: true, brightness: 95 }); await sleep(200); await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: false, brightness: 0 }); await sleep(200); } await setLight(ctx.sendRPC, WATER_BUTTON_MAC, true, 95); return; } // Remote switch goes offline — turn light off and cancel any timer if (ctx.trigger.mac === REMOTE_SWITCH_MAC && ctx.trigger.field === 'online' && ctx.trigger.event === false) { ctx.log('Remote switch went offline - turning light off'); await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: false, brightness: 0 }); const state = getState(WATER_BUTTON_MAC); if (state.timer) { clearTimeout(state.timer); state.timer = null; } state.countMode = false; state.timerStartedAt = null; state.timerEndsAt = null; return; } if (ctx.trigger.field !== 'button') return; // Ignore button events if remote switch is offline const remoteSwitchOnline = await ctx.getState(REMOTE_SWITCH_MAC, 'system', 'online'); if (remoteSwitchOnline !== true) { ctx.log('Button ignored - remote switch is offline'); return; } // ── Button events ──────────────────────────────────────────────────── const mac = ctx.trigger.mac; const state = getState(mac); ctx.log(`Event: ${ctx.trigger.event}, countMode: ${state.countMode}, storedDuration: ${state.storedDuration}ms`); if (ctx.trigger.event === 'btn_down') { const timerWasActive = state.timer !== null; if (state.timer) { clearTimeout(state.timer); state.timer = null; } state.timerStartedAt = null; state.timerEndsAt = null; if (state.countMode) { await setLight(ctx.sendRPC, mac, false, 0); const elapsed = Date.now() - state.countStart; state.storedDuration = elapsed; state.countMode = false; ctx.log(`Count mode ended. Stored duration: ${elapsed}ms`); ctx.updateStatus({ storedDuration: state.storedDuration, countMode: false, timerActive: false, lastAction: 'Duration saved' }); persistedState[mac] = { storedDuration: elapsed }; saveState(persistedState); await setLight(ctx.sendRPC, mac, true, 95); } else if (timerWasActive) { ctx.log('Timer cancelled by button press. Turning light on.'); await setLight(ctx.sendRPC, mac, true, 95); ctx.updateStatus({ storedDuration: state.storedDuration, countMode: false, timerActive: false, lastAction: 'Timer cancelled' }); } else { await setLight(ctx.sendRPC, mac, false, 0); ctx.log(`Light off. Will turn on in ${state.storedDuration}ms`); const updateStatus = ctx.updateStatus; updateStatus({ storedDuration: state.storedDuration, countMode: false, timerActive: true, lastAction: 'Timer started' }); state.timerStartedAt = Date.now(); state.timerEndsAt = Date.now() + state.storedDuration; state.timer = setTimeout(async () => { ctx.log('Timer elapsed. Turning light on.'); const rpc = global.__waterBotRPC; if (rpc) await setLight(rpc, mac, true, 95); state.timer = null; state.timerStartedAt = null; state.timerEndsAt = null; updateStatus({ storedDuration: state.storedDuration, countMode: false, timerActive: false, lastAction: 'Timer completed' }); }, state.storedDuration); } } // long_push — enter count mode if (ctx.trigger.event === 'long_push' && !state.countMode) { if (state.timer) { clearTimeout(state.timer); state.timer = null; } state.timerStartedAt = null; state.timerEndsAt = null; ctx.log('Entering count mode...'); for (let i = 0; i < 2; i++) { await ctx.sendRPC(mac, 'Light.Set', { id: 0, on: true, brightness: 95 }); await sleep(200); await ctx.sendRPC(mac, 'Light.Set', { id: 0, on: false, brightness: 0 }); await sleep(200); } await setLight(ctx.sendRPC, mac, false, 0); state.countMode = true; state.countStart = Date.now(); ctx.log('Count mode active. Light stays off. Press button to set duration and turn on.'); ctx.updateStatus({ storedDuration: state.storedDuration, countMode: true, timerActive: false, lastAction: 'Counting...' }); } } }; // ── HTTP Status Page ────────────────────────────────────────────────────────── startStatusServer({ getState, WATER_BUTTON_MAC, REMOTE_SWITCH_MAC, setLight, persistedState, saveState });