feat: add ON/OFF action config for Countdown and Away rules in Homebridge plugin
- Countdown: new 'Action when timer fires' dropdown (Turn ON / Turn OFF); scheduler uses rule.countdownAction instead of hardcoded 1/0 - Away Mode: scheduler now reads startAction/endAction from the rule (UI already exposed these fields); _stopAwayLoop respects endAction instead of always forcing OFF at window end Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -398,6 +398,13 @@
|
|||||||
<label>Countdown Duration (minutes)</label>
|
<label>Countdown Duration (minutes)</label>
|
||||||
<input type="number" id="dwm-countdown-mins" min="1" max="1440" placeholder="60" />
|
<input type="number" id="dwm-countdown-mins" min="1" max="1440" placeholder="60" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Action when timer fires</label>
|
||||||
|
<select id="dwm-countdown-action">
|
||||||
|
<option value="1">Turn ON (then auto-OFF after duration)</option>
|
||||||
|
<option value="0">Turn OFF (then auto-ON after duration)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dwm-alwayson-info" style="display:none;margin-bottom:12px;padding:10px;background:rgba(48,209,88,.1);border:1px solid rgba(48,209,88,.3);border-radius:6px;font-size:0.82rem;color:#4ade80">
|
<div id="dwm-alwayson-info" style="display:none;margin-bottom:12px;padding:10px;background:rgba(48,209,88,.1);border:1px solid rgba(48,209,88,.3);border-radius:6px;font-size:0.82rem;color:#4ade80">
|
||||||
|
|||||||
@@ -196,7 +196,8 @@ function dwmRuleSummary(r) {
|
|||||||
}
|
}
|
||||||
if (r.type === 'Countdown') {
|
if (r.type === 'Countdown') {
|
||||||
const mins = r.countdownTime ? Math.round(r.countdownTime / 60) : null;
|
const mins = r.countdownTime ? Math.round(r.countdownTime / 60) : null;
|
||||||
return mins ? `⏱ ${mins} min auto-off` : '—';
|
const action = r.countdownAction === 0 ? 'OFF → auto-ON' : 'ON → auto-OFF';
|
||||||
|
return mins ? `⏱ ${mins} min · ${action}` : '—';
|
||||||
}
|
}
|
||||||
const days = dayLabel(r.days);
|
const days = dayLabel(r.days);
|
||||||
const devs = (r.targetDevices ?? []).map((td) => esc(td.name ?? td.host)).join(', ') || 'no targets';
|
const devs = (r.targetDevices ?? []).map((td) => esc(td.name ?? td.host)).join(', ') || 'no targets';
|
||||||
@@ -442,6 +443,7 @@ function openDwmEdit(id) {
|
|||||||
document.getElementById('dwm-end-action').value = String(r.endAction ?? -1);
|
document.getElementById('dwm-end-action').value = String(r.endAction ?? -1);
|
||||||
document.getElementById('dwm-countdown-mins').value =
|
document.getElementById('dwm-countdown-mins').value =
|
||||||
r.countdownTime ? String(Math.round(r.countdownTime / 60)) : '';
|
r.countdownTime ? String(Math.round(r.countdownTime / 60)) : '';
|
||||||
|
document.getElementById('dwm-countdown-action').value = String(r.countdownAction ?? 1);
|
||||||
|
|
||||||
_selectedDwmDays = new Set((r.days ?? []).map(Number));
|
_selectedDwmDays = new Set((r.days ?? []).map(Number));
|
||||||
|
|
||||||
@@ -475,6 +477,7 @@ function openDwmEdit(id) {
|
|||||||
document.getElementById('dwm-start-action').value = '1';
|
document.getElementById('dwm-start-action').value = '1';
|
||||||
document.getElementById('dwm-end-action').value = '-1';
|
document.getElementById('dwm-end-action').value = '-1';
|
||||||
document.getElementById('dwm-countdown-mins').value = '';
|
document.getElementById('dwm-countdown-mins').value = '';
|
||||||
|
document.getElementById('dwm-countdown-action').value = '1';
|
||||||
document.getElementById('dwm-trigger-src').value = '';
|
document.getElementById('dwm-trigger-src').value = '';
|
||||||
document.getElementById('dwm-trigger-event').value = 'any';
|
document.getElementById('dwm-trigger-event').value = 'any';
|
||||||
document.getElementById('dwm-trigger-action').value = 'on';
|
document.getElementById('dwm-trigger-action').value = 'on';
|
||||||
@@ -605,6 +608,7 @@ document.getElementById('dwm-form-save-btn').addEventListener('click', async ()
|
|||||||
const mins = Number(document.getElementById('dwm-countdown-mins').value);
|
const mins = Number(document.getElementById('dwm-countdown-mins').value);
|
||||||
if (!mins || mins < 1) { showModalError('Enter countdown duration in minutes'); return; }
|
if (!mins || mins < 1) { showModalError('Enter countdown duration in minutes'); return; }
|
||||||
rule.countdownTime = mins * 60;
|
rule.countdownTime = mins * 60;
|
||||||
|
rule.countdownAction = Number(document.getElementById('dwm-countdown-action').value);
|
||||||
} else {
|
} else {
|
||||||
const startType = document.getElementById('dwm-start-type').value;
|
const startType = document.getElementById('dwm-start-type').value;
|
||||||
const startOffset = parseInt(document.getElementById('dwm-start-offset').value ?? '0', 10) || 0;
|
const startOffset = parseInt(document.getElementById('dwm-start-offset').value ?? '0', 10) || 0;
|
||||||
|
|||||||
@@ -189,6 +189,8 @@ class DwmScheduler {
|
|||||||
const startSecs = resolveSecs(Number(rule.startTime ?? -1), rule.startType, rule.startOffset, todaySun);
|
const startSecs = resolveSecs(Number(rule.startTime ?? -1), rule.startType, rule.startOffset, todaySun);
|
||||||
const endSecs = resolveSecs(Number(rule.endTime ?? -1), rule.endType, rule.endOffset, todaySun);
|
const endSecs = resolveSecs(Number(rule.endTime ?? -1), rule.endType, rule.endOffset, todaySun);
|
||||||
if (startSecs === null) continue;
|
if (startSecs === null) continue;
|
||||||
|
const awayStartAction = Number(rule.startAction ?? 1);
|
||||||
|
const awayEndAction = Number(rule.endAction ?? 0);
|
||||||
|
|
||||||
for (const dayId of (rule.days ?? [])) {
|
for (const dayId of (rule.days ?? [])) {
|
||||||
const td0 = rule.targetDevices?.[0];
|
const td0 = rule.targetDevices?.[0];
|
||||||
@@ -196,14 +198,14 @@ class DwmScheduler {
|
|||||||
ruleId: rule.id, ruleName: rule.name,
|
ruleId: rule.id, ruleName: rule.name,
|
||||||
targetHost: td0?.host ?? '', targetPort: td0?.port ?? 0,
|
targetHost: td0?.host ?? '', targetPort: td0?.port ?? 0,
|
||||||
dayId: Number(dayId), targetSecs: startSecs,
|
dayId: Number(dayId), targetSecs: startSecs,
|
||||||
action: 1, isAwayStart: true,
|
action: awayStartAction, isAwayStart: true,
|
||||||
});
|
});
|
||||||
if (endSecs !== null && endSecs >= 0) {
|
if (endSecs !== null && endSecs >= 0) {
|
||||||
schedule.push({
|
schedule.push({
|
||||||
ruleId: rule.id + '-away-end', ruleName: rule.name,
|
ruleId: rule.id + '-away-end', ruleName: rule.name,
|
||||||
targetHost: td0?.host ?? '', targetPort: td0?.port ?? 0,
|
targetHost: td0?.host ?? '', targetPort: td0?.port ?? 0,
|
||||||
dayId: Number(dayId), targetSecs: endSecs,
|
dayId: Number(dayId), targetSecs: endSecs,
|
||||||
action: 0, isAwayEnd: true, awayRuleId: rule.id,
|
action: awayEndAction, isAwayEnd: true, awayRuleId: rule.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,18 +218,20 @@ class DwmScheduler {
|
|||||||
const windowEnd = Number(rule.windowEnd ?? -1);
|
const windowEnd = Number(rule.windowEnd ?? -1);
|
||||||
if (windowStart < 0 || !(rule.windowDays?.length)) continue;
|
if (windowStart < 0 || !(rule.windowDays?.length)) continue;
|
||||||
|
|
||||||
|
const countdownAction = Number(rule.countdownAction ?? 1);
|
||||||
|
const countdownEnd = countdownAction === 1 ? 0 : 1; // opposite of start
|
||||||
const crossesMidnight = windowEnd >= 0 && windowEnd < windowStart;
|
const crossesMidnight = windowEnd >= 0 && windowEnd < windowStart;
|
||||||
for (const dayId of rule.windowDays) {
|
for (const dayId of rule.windowDays) {
|
||||||
for (const td of (rule.targetDevices ?? [])) {
|
for (const td of (rule.targetDevices ?? [])) {
|
||||||
if (!td.host || !td.port) continue;
|
if (!td.host || !td.port) continue;
|
||||||
schedule.push({ ruleId: rule.id, ruleName: rule.name,
|
schedule.push({ ruleId: rule.id, ruleName: rule.name,
|
||||||
targetHost: td.host, targetPort: td.port,
|
targetHost: td.host, targetPort: td.port,
|
||||||
dayId: Number(dayId), targetSecs: windowStart, action: 1 });
|
dayId: Number(dayId), targetSecs: windowStart, action: countdownAction });
|
||||||
if (windowEnd >= 0) {
|
if (windowEnd >= 0) {
|
||||||
const offDayId = crossesMidnight ? (Number(dayId) % 7) + 1 : Number(dayId);
|
const offDayId = crossesMidnight ? (Number(dayId) % 7) + 1 : Number(dayId);
|
||||||
schedule.push({ ruleId: rule.id + '-wend', ruleName: rule.name,
|
schedule.push({ ruleId: rule.id + '-wend', ruleName: rule.name,
|
||||||
targetHost: td.host, targetPort: td.port,
|
targetHost: td.host, targetPort: td.port,
|
||||||
dayId: offDayId, targetSecs: windowEnd, action: 0 });
|
dayId: offDayId, targetSecs: windowEnd, action: countdownEnd });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,12 +348,14 @@ class DwmScheduler {
|
|||||||
if (loop.timer) clearTimeout(loop.timer);
|
if (loop.timer) clearTimeout(loop.timer);
|
||||||
this._awayLoops.delete(ruleId);
|
this._awayLoops.delete(ruleId);
|
||||||
if (forceOff) {
|
if (forceOff) {
|
||||||
|
const endAction = Number(loop.rule.endAction ?? 0);
|
||||||
|
const turnOn = endAction === 1;
|
||||||
for (const td of loop.devices) {
|
for (const td of loop.devices) {
|
||||||
this._wemo.setBinaryState(td.host, td.port, false).catch(() => {});
|
this._wemo.setBinaryState(td.host, td.port, turnOn).catch(() => {});
|
||||||
}
|
}
|
||||||
this._emit({ success: true,
|
this._emit({ success: true,
|
||||||
msg: `"${loop.rule.name}" Away Mode window ended — all devices OFF`,
|
msg: `"${loop.rule.name}" Away Mode window ended — all devices ${turnOn ? 'ON' : 'OFF'}`,
|
||||||
entry: { action: 0 } });
|
entry: { action: endAction } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user