diff --git a/packages/homebridge-plugin/homebridge-ui/public/index.html b/packages/homebridge-plugin/homebridge-ui/public/index.html
index 3fb7f20..e51f441 100644
--- a/packages/homebridge-plugin/homebridge-ui/public/index.html
+++ b/packages/homebridge-plugin/homebridge-ui/public/index.html
@@ -394,10 +394,6 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
Leave window blank to run at any time.
diff --git a/packages/homebridge-plugin/homebridge-ui/public/index.js b/packages/homebridge-plugin/homebridge-ui/public/index.js
index 05178ac..938402c 100644
--- a/packages/homebridge-plugin/homebridge-ui/public/index.js
+++ b/packages/homebridge-plugin/homebridge-ui/public/index.js
@@ -195,9 +195,12 @@ function dwmRuleSummary(r) {
return `⚡ If ${src} → ${when}, then ${action} (${targets})`;
}
if (r.type === 'Countdown') {
- const mins = r.countdownTime ? Math.round(r.countdownTime / 60) : null;
+ const mins = r.countdownTime ? Math.round(r.countdownTime / 60) : null;
const cond = r.countdownAction === 'off_to_on' ? 'OFF→ON' : 'ON→OFF';
- return mins ? `⏱ ${mins} min · ${cond}` : '—';
+ const win = (r.windowStart >= 0 && r.windowEnd >= 0)
+ ? ` · ${secsToHHMM(r.windowStart)}–${secsToHHMM(r.windowEnd)}`
+ : (r.windowStart >= 0 ? ` · from ${secsToHHMM(r.windowStart)}` : '');
+ return mins ? `⏱ ${mins} min · ${cond}${win}` : '—';
}
const days = dayLabel(r.days);
const devs = (r.targetDevices ?? []).map((td) => esc(td.name ?? td.host)).join(', ') || 'no targets';
@@ -443,7 +446,9 @@ function openDwmEdit(id) {
document.getElementById('dwm-end-action').value = String(r.endAction ?? -1);
document.getElementById('dwm-countdown-mins').value =
r.countdownTime ? String(Math.round(r.countdownTime / 60)) : '';
- document.getElementById('dwm-countdown-action').value = r.countdownAction ?? 'on_to_off';
+ document.getElementById('dwm-countdown-action').value = r.countdownAction ?? 'on_to_off';
+ document.getElementById('dwm-countdown-window-start').value = r.windowStart >= 0 ? secsToHHMM(r.windowStart) : '';
+ document.getElementById('dwm-countdown-window-end').value = r.windowEnd >= 0 ? secsToHHMM(r.windowEnd) : '';
_selectedDwmDays = new Set((r.days ?? []).map(Number));
@@ -476,8 +481,10 @@ function openDwmEdit(id) {
document.getElementById('dwm-end-time').value = '';
document.getElementById('dwm-start-action').value = '1';
document.getElementById('dwm-end-action').value = '-1';
- document.getElementById('dwm-countdown-mins').value = '';
- document.getElementById('dwm-countdown-action').value = 'on_to_off';
+ document.getElementById('dwm-countdown-mins').value = '';
+ document.getElementById('dwm-countdown-action').value = 'on_to_off';
+ document.getElementById('dwm-countdown-window-start').value = '';
+ document.getElementById('dwm-countdown-window-end').value = '';
document.getElementById('dwm-trigger-src').value = '';
document.getElementById('dwm-trigger-event').value = 'any';
document.getElementById('dwm-trigger-action').value = 'on';
@@ -609,6 +616,10 @@ document.getElementById('dwm-form-save-btn').addEventListener('click', async ()
if (!mins || mins < 1) { showModalError('Enter countdown duration in minutes'); return; }
rule.countdownTime = mins * 60;
rule.countdownAction = document.getElementById('dwm-countdown-action').value;
+ const winStart = hhmmToSecs(document.getElementById('dwm-countdown-window-start').value);
+ const winEnd = hhmmToSecs(document.getElementById('dwm-countdown-window-end').value);
+ rule.windowStart = winStart >= 0 ? winStart : -1;
+ rule.windowEnd = winEnd >= 0 ? winEnd : -1;
} else {
const startType = document.getElementById('dwm-start-type').value;
const startOffset = parseInt(document.getElementById('dwm-start-offset').value ?? '0', 10) || 0;
diff --git a/packages/homebridge-plugin/lib/scheduler.js b/packages/homebridge-plugin/lib/scheduler.js
index 0c38d12..6cca479 100644
--- a/packages/homebridge-plugin/lib/scheduler.js
+++ b/packages/homebridge-plugin/lib/scheduler.js
@@ -587,16 +587,30 @@ class DwmScheduler {
}
}
- // ── Countdown — fire only when state matches configured condition ──
+ // ── Countdown — fire only when state matches condition and within window ──
if (countdownDevMap.has(key)) {
const prevState = this._countdownStates.get(key);
this._countdownStates.set(key, isOn);
if (prevState !== undefined && prevState !== isOn) {
+ const nowSecs = secondsFromMidnight(new Date());
for (const { rule, td } of countdownDevMap.get(key)) {
const condition = rule.countdownAction ?? 'on_to_off';
const triggered = condition === 'on_to_off' ? isOn : !isOn;
if (!triggered) continue; // state doesn't match this rule's condition
+ // Check active window (if defined)
+ const winStart = Number(rule.windowStart ?? -1);
+ const winEnd = Number(rule.windowEnd ?? -1);
+ if (winStart >= 0 && winEnd >= 0) {
+ const crossesMidnight = winEnd < winStart;
+ const inWindow = crossesMidnight
+ ? (nowSecs >= winStart || nowSecs <= winEnd)
+ : (nowSecs >= winStart && nowSecs <= winEnd);
+ if (!inWindow) continue; // outside active window
+ } else if (winStart >= 0) {
+ if (nowSecs < winStart) continue;
+ }
+
const timerKey = `${key}-${rule.id}`;
// Cancel any pending timer for this device+rule
const existing = this._countdownTimers.get(timerKey);