第 13 章

人體感測、燈光與防連續觸發實戰

可靠的感測照明不是「偵測到活動就開燈,幾分鐘後關燈」而已。你必須把狀態邊界、重複事件、等待期間的新活動、手動操作、重啟與 unavailable 都畫成明確分支。本章用停用的 Home Assistant 節點與假資料,完成不碰真實燈具的分階段設計。

為何需要本章

人體感測器可能在短時間內反覆切換,也可能只在偵測結束後才回到無活動;燈光狀態可能被牆壁開關、Home Assistant、另一條自動化或斷線恢復改變。若流程只有一條固定 Delay,舊的「關燈」計時可能在新活動發生後仍然抵達,形成競態;若每次活動都建立新的等待,又會累積多條同時存在的訊息。

本章採用具體但安全的情境:非關鍵區域的走道燈。活動成立時只產生「建議開燈」訊息;活動結束並持續一段時間後只產生「建議關燈」訊息。兩個真正 Action 都保持停用,target 為 PLACEHOLDER_LIGHT_ENTITY,感測器為 PLACEHOLDER_MOTION_ENTITY,不含任何真實環境 ID。

不可把本流程用於逃生照明、保全、醫療或其他安全關鍵用途。感測遺漏、Home Assistant unavailable、Node-RED 重啟或網路斷線都可能使結果與現場不同。所有 HA 監聽、查詢、等待與動作節點在範例規劃中均停用;Action 不接線、不部署執行。

把「事件、狀態、意圖、動作」分開

層次範例資料流程責任不可假設
事件感測實體收到一次 state_changed辨識來源並保留 old/new 邊界事件必定代表人仍在場
狀態活動、無活動、unknown、unavailable正規化並拒絕未知值所有 binary sensor 都用相同顯示文字
意圖建議開燈、候選關燈、取消關燈去重、冷卻、手動覆寫與競態處理意圖已在裝置完成
動作呼叫核准的開燈或關燈 action最後一刻重查條件並限制 target呼叫一定成功或有通用回應格式

debounce 是要求輸入穩定一段時間才承認轉換;cooldown 是某次意圖產生後,在一段時間內抑制同類意圖;延後關燈 則是業務等待。三者用途不同。把全部都叫 Delay,會讓你無法回答新事件是重設、排隊、丟棄還是並行。

occupancy 也不是 motion 的同義詞。單一被動紅外線感測器通常只能提供某種活動狀態,不能證明房間無人;多感測器聚合又可能揭露家庭作息。流程與 Debug 使用「活動訊號」而非「某人在家」,不保存精細時間序列超過排障需要。

七步建立可撤回的安全流程

  1. 定義狀態字典。

    先在不連線的文件中記錄感測整合實際可能值,映射成 ACTIVE、CLEAR、INVALID。unknown、unavailable、null、缺少 new_state 一律是 INVALID,不可等同 CLEAR。

  2. 選擇 Events: state 或 Trigger: state。

    只需單一實體轉換與基本持續時間時選 Events: state;需要 allowed/blocked 條件、多條自訂輸出或可輸入測試事件時才選 Trigger: state。兩者擇一作主入口,不要同時監聽造成雙觸發。

  3. 先畫純訊息路由。

    使用手動 Inject、Change、Switch 與 Debug,建立 ACTIVE、CLEAR、INVALID、重複 ACTIVE 與亂序時間戳。可參考02-switch-routing.json,但不要加入 HA 節點或真實 ID。

  4. 加入可取消的等待。

    CLEAR 只建立候選關燈;新 ACTIVE 必須取消或取代舊候選。去抖可使用核心 Trigger 的 extend delay 並設定 bytopic;若用固定 Delay 延後候選,每次替換前先明確送 msg.reset,而且每個區域使用獨立 Delay。也可使用 Wait Until 等待條件;不要誤以為普通新訊息會自動重設 Delay。

  5. 加入手動覆寫與最後重查。

    牆壁開關或人工控制後設定有限期限的 manual_override。候選關燈到期時,再查活動狀態、燈光狀態、覆寫旗標與資料新鮮度,任何一項不確定都不動作。

  6. 分階段連到測試環境。

    第一階段所有 HA 節點停用;第二階段若另獲核准,只啟用一個唯讀事件節點,下游仍是遮罩 Debug;第三階段在非正式時段用測試實體驗證。正式 Action 仍停用。

  7. 記錄重啟與回復。

    確認 context 是否持久化、等待中的訊息在部署後是否消失、重連後是否產生初始輸出。無法證明當前狀態時,流程回到「等待下一次可信事件」而不是猜測並關燈。

走道照明的狀態機

以下是設計規格,不是可直接匯入的 Flow,也不代表任何節點已啟用:

停用的 Events: state / Trigger: state
  ├─ ACTIVE → 取消候選關燈 → 檢查冷卻與手動覆寫 → 建議開燈 → 停用 Action
  ├─ CLEAR  → debounce 穩定 → 建立單一候選關燈 → 等待/可被 ACTIVE 取消
  └─ INVALID → 取消候選關燈 → 遮罩後的診斷 Debug

候選關燈到期
  → Current State 重查活動、燈光、資料時間、manual_override
  ├─ 全部明確安全 → 建議關燈 → 停用 Action
  └─ 任一不明確   → 不動作 → 診斷 Debug

開燈路徑也不應每個 ACTIVE 都呼叫一次。先確認燈目前是否已開、最近是否已送同一意圖,以及 manual override 是否要求保持現況。關燈路徑更保守:只有 CLEAR 持續、燈仍為自動化先前開啟、沒有新活動且沒有人工覆寫時,才產生「建議關燈」。

手動覆寫的最小契約

  • 覆寫值必須有來源、建立時間與到期時間;不保存人名或位置。
  • 人工把燈打開時,可選擇在覆寫期限內禁止自動關燈;人工把燈關閉時,禁止活動訊號立刻反向打開。
  • 覆寫到期不等於立即執行動作,只代表恢復評估;仍須等下一次可信事件或重新查詢。
  • Node-RED 重啟後若覆寫資料不存在或過期,採「不自動關燈」的保守預設。

若不同入口都能控制同一盞燈,必須指定單一協調者,或用共同的意圖/覆寫 context。否則感測流程、排程流程與人工操作會互相覆蓋。流程架構與 context 壽命可再參考第 7 章時間與 Context。

Events: state 與 Trigger: state 的選擇

Events: state:最窄的狀態轉換

當需求可以寫成「指定實體的新狀態由已知值轉成 ACTIVE/CLEAR,並選擇是否要求持續時間」,Events: state 較容易審查。開始前先閱讀第 9 章狀態事件邊界,並參考停用監聽節點的04-events-state.json。該範例只展示 0.80.3 的持久化欄位,不包含本章的真實感測器或燈。

Trigger: state:有條件的 allowed/blocked 路由

0.80.3 的 Trigger: state 可選 exact、list、substring 或 regex 實體篩選,設定 conditions,預設以 allowed 與 blocked 兩個輸出表示所有條件是否通過,也可建立自訂輸出。它還有 Output on connect、Enable input 與測試訊息能力。彈性越大,越需要限制邊界:

  • 正式流程優先 exact;substring/regex 可能在新增實體後意外擴大範圍。
  • Output on connect 會在連線/部署時產生初始輸出,感測照明預設關閉,避免重啟被當成新活動。
  • Enable input 預設不需要;若為離線測試開啟,只接受固定 schema 的假事件,測完關閉。
  • 條件失敗應接 blocked 診斷分支,不得把 blocked 當成「應關燈」。條件失敗只表示不允許目前意圖。
  • State Type 轉型不應拿 unknown/unavailable 做數值或布林推論;先以原始字串隔離異常,再轉換已知值。
測試輸入不是正式感測來源。Trigger: state 可以接收含 entity_id、old_state、new_state 的訊息來模擬事件,但這只驗證節點條件與輸出,不證明 Home Assistant 整合、無線傳輸或真實燈具會照樣運作。假事件只能使用佔位 ID,且節點保持停用直到隔離測試獲核准。

兩種節點輸出的時間戳仍可能反映來源延遲。正規化層要保留事件時間與接收時間,拒絕明顯倒退或超出新鮮度門檻的資料。不要用 Debug 保存完整 old_state/new_state;只顯示匿名來源、正規化狀態與延遲級距。

Delay、Wait Until、去抖與競態

模式 A:Trigger 去抖,或明確 reset 的固定 Delay

Node-RED 5.0.2 的固定 Delay 會為每一則普通訊息建立獨立 timeout;新訊息不會自動重設舊計時,也沒有 per-topic reset。msg.reset 會一次清除該 Delay 節點全部待送的固定延遲訊息。因此 debounce 應使用核心 Trigger 的 extend delay 並設定 bytopic;若固定 Delay 用於 CLEAR 後的業務等待,替換候選前先明確送 reset,再送唯一候選,ACTIVE 也送 reset,且每個區域使用獨立 Delay,避免一區清除另一區。部署或程序重啟仍可能讓記憶體等待消失,所以不能把「沒有計時器」解讀為可關燈。

模式 B:Wait Until 等狀態條件

Wait Until 在收到輸入後,監聽指定實體直到 property 與 comparator/value 相符。0.80.3 支援 timeout;成功與逾時是不同輸出。勾選 Check against current state 時,只有單一 exact 實體可立即檢查目前狀態。每個節點只有一個 active slot;後來輸入會取消現有 timer 並取代保存的 message/config,而不是排隊。它也能由 msg.payload 覆寫 entities、property、comparator、value、timeout 等欄位;安全流程應開啟 Block Input Overrides,避免上游改變監聽對象或逾時。timeout 0 不建立 timer,不能當作有限等待。

08-wait-until.json 提供停用的 0.80.3 Wait Until 節點,使用 PLACEHOLDER_HA_SERVER_CONFIG 與 PLACEHOLDER_ENTITY_ID。它設定 10 秒正 timeout 與兩個輸出:成功只接有限欄位 Debug,逾時輸出留空。這只是欄位審查,不要直接把它當照明流程;匯入後仍不可一次啟用,且逾時輸出不得直連 Action。

機制解決問題新 ACTIVE 到來重啟後安全預設
輸入 debounce短暫抖動重新計算穩定時間等待新的完整穩定窗
開燈 cooldown重複 ACTIVE 呼叫更新觀察時間,不重送相同意圖先查現況,不立即補送
獨立固定 Delay+明確 resetCLEAR 後延後關燈msg.reset 清除該節點全部 pending視為沒有可證明的候選,不關燈
Wait Until等待明確狀態或逾時依狀態條件完成或由另一路 reset重新評估但不假定舊等待仍存在
最後 Current State避免舊訊息控制新現況若已變 ACTIVE 就阻擋查不到或 unavailable 就不動作

競態與重新啟動

每個候選關燈帶一個單調遞增 generation 或 request ID。ACTIVE 會使 generation 前進;等待結束時若訊息 generation 不是目前值,直接丟棄。這能防止「取消訊息與到期訊息同時抵達」的競態,但 generation 本身不是裝置狀態,最後仍要 Current State 重查。

若選用可持久化 context,必須處理舊資料版本、到期時間與備份隱私;若使用記憶體 context,重啟會遺失候選與覆寫。兩種都不能默默假設安全,應在狀態機中寫出 restart 分支。

四階段測試與 Action 保險

階段 0:離線審查

閱讀 JSON、節點設定與接線,不開 Node-RED 連線。建立測試矩陣:ACTIVE、重複 ACTIVE、CLEAR 抖動、CLEAR 後再 ACTIVE、unknown、unavailable、old_state 缺失、重啟、manual override。所有 ID 都是佔位符。

階段 1:純核心假資料

手動 Inject 送出正規化狀態,不使用自動 repeat 或 once。Debug 只顯示 scenario、decision、generation,確認每個案例最多產生一個意圖。這一階段沒有 Home Assistant Server config,也沒有外部副作用。

階段 2:唯讀觀察

獲得另行核准後,在隔離 Home Assistant 測試實體上一次只啟用一個 Events: state 或 Trigger: state;Current State 與 Wait Until 仍可保持停用,Action 必須停用。比對事件順序、抖動間隔與 unavailable 行為,不記錄居住者活動細節。

階段 3:停用 Action 的端到端演練

以07-action.json作欄位參考,Action 節點設定 PLACEHOLDER_HA_ACTION、PLACEHOLDER_ENTITY_ID,保留 d: true,Block Input Overrides 開啟。上游只到「準備動作」Debug,不接 Action。不得把範例中的訊息資料當成照明服務的通用 schema。

  • 開燈與關燈使用不同意圖類型,各自有冷卻與診斷紀錄。
  • target 固定為單一核准測試實體,不使用 area、device、floor 或 label 擴大範圍。
  • data 只含經該 action schema 核對的最少欄位,不接受任意上游合併。
  • Action 沒有通用「裝置已完成」保證;驗證要靠後續狀態與容忍時間,但不能形成無限重試。
驗收標準:所有異常案例都只產生不動作;CLEAR 後的新 ACTIVE 能取消舊候選;重啟後不會自動關燈;manual override 期間不會產生反向意圖;Action 全程停用。未達任一條就停止,不進正式環境。

故障排除

  • 一次活動產生多個開燈意圖:確認是否同時使用 Events: state 與 Trigger: state、是否監聽屬性更新、是否缺少狀態變更篩選。停用所有 Action,以事件時間與匿名來源計數,再加入去重與 cooldown。
  • 人在移動時燈仍收到關燈意圖:檢查新 ACTIVE 是否真的送出 msg.reset、各區是否使用獨立 Delay、替換前是否先清除全部 pending、generation 是否過期,以及到期前是否重查狀態。固定 Delay 沒有 per-topic reset;任何不確定都改為不動作。
  • Wait Until 永遠等待:確認 entities 篩選、property 路徑、原始狀態字串與 comparator;設定有限 timeout 並接獨立逾時分支。不要把逾時輸出接關燈,timeout 只代表未觀察到條件。
  • Wait Until 立即完成但預期等待:若單一 exact 實體且 Check against current state 已啟用,現況符合時會立即完成。確認這是否符合規格;不應靠關閉檢查來掩蓋舊狀態問題。
  • 部署或重啟後行為改變:等待與記憶體 context 可能已清除,Trigger: state 的 Output on connect 也可能產生初始輸出。保持該選項關閉,重啟分支先查狀態並禁止自動關燈。
  • unknown/unavailable 被當成無活動:檢查 Switch 規則順序與型別轉換。INVALID 必須先被攔截並取消候選,不能落入 default CLEAR;修正後用兩個字串案例重新測試。
  • 人工關燈後自動化立即開回:manual override 沒有在入口前檢查,或沒有記錄人工來源。建立有限期限覆寫,期間只觀察活動,不產生反向 Action。

固定來源與相關節點

本章聚焦 Trigger: state 與 Wait Until 的輸入、條件、輸出與安全邊界。Events: state、Current State、Delay 與 Action 是情境所需的相鄰元件;詳細設定分別連回第 9、10、11 章及安全 Flow 範例,本章不外推未查證的 UI 或服務回應。

常見問題

感測器變成 unavailable 時,可以開始關燈倒數嗎?
不可以。unavailable 表示資料不可用,不代表無活動。取消候選關燈、留下遮罩診斷,等下一個可信狀態或人工處理。
Delay、Trigger 和 Wait Until 應該選哪一個?
輸入 debounce 用 Trigger 的 extend delay 與 bytopic。固定延後可用每區獨立 Delay,但替換前必須明確送 msg.reset;普通新訊息不會重設 timer,reset 會清掉該節點全部 pending。需要等待實體 property 並區分成功/逾時則用 Wait Until。三者都要有重啟與競態策略。
Trigger: state 的 blocked 輸出可以直接關燈嗎?
不可以。blocked 只表示一個或多個條件未通過,不等同「安全關燈」。它應進診斷或忽略分支。
為何關燈前還要 Current State?
等待期間可能有新活動、人工操作或重連,舊訊息已不代表現況。最後查詢是必要保險;查不到、過期或 unavailable 時仍不動作。
可以用區域內所有感測器的 regex 嗎?
正式流程優先列出 exact 實體。regex 或 substring 可能在新增實體後擴大監聽,也更難審查;若確實使用,必須有命名治理與變更測試。
Action 節點何時可以啟用?
本章不授權啟用。先完成假資料、唯讀觀察、競態、重啟、人工覆寫與 unavailable 驗收,再依組織程序由另一位審查者核准測試 target;正式部署仍是另一步。