Files
shellySrv/rules/waterButtonLearnAndCall.js

284 lines
11 KiB
JavaScript

/**
* 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)
*/
import fs from 'fs';
import path from 'path';
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';
// Helper to set local light and sync remote switch inversely
async function setLight(ctx, mac, on, brightness) {
await ctx.sendRPC(mac, 'Light.Set', { id: 0, on, brightness });
// Remote switch is inverse: light off = switch on, light on = switch off
await ctx.sendRPC(REMOTE_SWITCH_MAC, 'Switch.Set', { id: 0, on: !on });
}
// Load persisted state
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 {};
}
// Save state to file
function saveState(state) {
try {
// Only save storedDuration per MAC
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);
}
}
// State for devices (in memory, with persistence for storedDuration)
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, // Default 5 seconds
timer: null
});
}
return deviceState.get(mac);
}
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) { // Max 5 minutes
const state = getState(WATER_BUTTON_MAC);
state.storedDuration = duration;
// Persist
persistedState[WATER_BUTTON_MAC] = { storedDuration: duration };
saveState(persistedState);
console.log(`[Rule] storedDuration set to ${duration}ms`);
return true;
}
}
return false;
},
async run(ctx) {
// Auto-on for water button when it connects (only if remote switch is 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');
// Double flash to indicate both devices are connected
ctx.log('Double flashing to confirm connection');
for (let i = 0; i < 2; i++) {
await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: true, brightness: 20 });
await sleep(200);
await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: false, brightness: 0 });
await sleep(200);
}
// Turn light on after flash (ready state)
await setLight(ctx, ctx.trigger.mac, true, 20);
} 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;
}
// Auto-off for remote switch when it connects
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 });
// Double flash the light
for (let i = 0; i < 2; i++) {
await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: true, brightness: 20 });
await sleep(200);
await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: false, brightness: 0 });
await sleep(200);
}
// Turn light on after flash
await setLight(ctx, WATER_BUTTON_MAC, true, 20);
return;
}
// Turn off light when remote switch goes offline
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 });
// Clear any pending timer
const state = getState(WATER_BUTTON_MAC);
if (state.timer) {
clearTimeout(state.timer);
state.timer = null;
}
state.countMode = false;
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;
}
const mac = ctx.trigger.mac;
const state = getState(mac);
ctx.log(`Event: ${ctx.trigger.event}, countMode: ${state.countMode}, storedDuration: ${state.storedDuration}ms`);
// Handle btn_down
if (ctx.trigger.event === 'btn_down') {
// Check if timer was active before clearing
const timerWasActive = state.timer !== null;
// Clear any pending timer
if (state.timer) {
clearTimeout(state.timer);
state.timer = null;
}
if (state.countMode) {
// We're in count mode - calculate elapsed time and turn light on
// Turn light off first (remote switch turns on)
await setLight(ctx, mac, false, 0);
const elapsed = Date.now() - state.countStart;
state.storedDuration = elapsed;
state.countMode = false;
ctx.log(`Count mode ended. Stored duration: ${elapsed}ms`);
// Push status update to dashboard
ctx.updateStatus({
storedDuration: state.storedDuration,
countMode: false,
timerActive: false,
lastAction: 'Duration saved'
});
// Persist the new duration
persistedState[mac] = { storedDuration: elapsed };
saveState(persistedState);
// Turn light on immediately (remote switch turns off)
await setLight(ctx, mac, true, 20);
} else if (timerWasActive) {
// Timer was running - cancel it and turn light on immediately
ctx.log('Timer cancelled by button press. Turning light on.');
await setLight(ctx, mac, true, 20);
// Push status update to dashboard
ctx.updateStatus({
storedDuration: state.storedDuration,
countMode: false,
timerActive: false,
lastAction: 'Timer cancelled'
});
} else {
// Normal mode - turn off light and schedule it to turn on after stored duration
await setLight(ctx, mac, false, 0);
ctx.log(`Light off. Will turn on in ${state.storedDuration}ms`);
// Capture updateStatus for use in timer callback
const updateStatus = ctx.updateStatus;
// Push status update to dashboard
updateStatus({
storedDuration: state.storedDuration,
countMode: false,
timerActive: true,
lastAction: 'Timer started'
});
state.timer = setTimeout(async () => {
ctx.log(`Timer elapsed. Turning light on.`);
await setLight(ctx, mac, true, 20);
state.timer = null;
// Push timer completed status
updateStatus({
storedDuration: state.storedDuration,
countMode: false,
timerActive: false,
lastAction: 'Timer completed'
});
}, state.storedDuration);
}
}
// Handle long_push - enter count mode
if (ctx.trigger.event === 'long_push' && !state.countMode) {
// Clear any pending timer
if (state.timer) {
clearTimeout(state.timer);
state.timer = null;
}
ctx.log('Entering count mode...');
// Flash to confirm (don't sync remote during flash)
for (let i = 0; i < 2; i++) {
await ctx.sendRPC(mac, 'Light.Set', { id: 0, on: true, brightness: 20 });
await sleep(200);
await ctx.sendRPC(mac, 'Light.Set', { id: 0, on: false, brightness: 0 });
await sleep(200);
}
// Ensure light stays off during count mode (remote switch on)
await setLight(ctx, mac, false, 0);
// Enter count mode
state.countMode = true;
state.countStart = Date.now();
ctx.log('Count mode active. Light stays off. Press button to set duration and turn on.');
// Push status update to dashboard
ctx.updateStatus({
storedDuration: state.storedDuration,
countMode: true,
timerActive: false,
lastAction: 'Counting...'
});
}
}
};