第 9 章

Events: state 與狀態改變

Events: state 在 HA 的 state_changed 事件抵達時輸出,適合用來辨認「何時改變」;它不是輪詢,也不是重新查歷史。本章以 HA WebSocket 0.80.3 的 persisted type(匯出 flow JSON 中保存的節點 type)server-state-changed、節點版本 6 為準,完整處理 selector、old/new state、型別、輸出、持續時間與重新連線邊界。

為何需要本章

狀態事件常被誤讀為只有「開」與「關」。在 Home Assistant 原始 state_changed schema 中,一筆事件可能只改 attributes,也可能在 entity 建立時沒有舊狀態、刪除時以 new_state: null 表示;unknown 與 unavailable 又是存在的字串狀態,不等於缺少物件。不過本章固定的套件版本不會把刪除事件交給 Events: state,下文會明確區分原始 schema 與節點實際可收到的事件。

Events: state 是零輸入的事件節點。它訂閱符合 entity selector 的事件,依忽略條件、only-change、If State 與 For 判斷,最後依 Output Properties 建立新的 msg。這與收到 msg 才讀 cache 的 Current State 不同,也與定時讀取的 Poll State 不同。

本章只做非執行審查:範例節點維持停用,Server 與 entity 都是 PLACEHOLDER_*。先閱讀 JSON、確認 selector 與輸出,不連線、不部署、不接 Action;任何環境操作都必須在你完成現場審查後另行決定。

v6 的處理順序與節點選型

0.80.3 的 v6 controller 可用下列順序理解:先確認節點啟用且 HA 正在 running,再檢查 entity selector 與五種 ignore 條件;需要時轉換 old/new state;一般事件才套用 only-change;接著計算 If State 與 For;最後將指定的 Output Properties 寫入訊息,If State 成立走第一輸出,不成立則在設定了條件時走第二輸出。

節點persisted type何時讀取本章界線
Events: stateserver-state-changed符合的 state_changed 抵達時本章主角;事件驅動,處理 old/new
Poll Statepoll-state依 interval 週期讀取只有事件來源不適用時才評估;限制頻率,詳見第 22 章
Trigger: statetrigger-state狀態事件符合 constraints 時較複合的 trigger/constraints;實戰見第 13 章
Current Stateapi-current-state收到上游 msg 時查 cache單次查詢與條件,見第 10 章

上表同時比較 Events: state、Poll State 與 Trigger: state,方便依觸發需求選擇。選 Poll State 不會讓資料更「即時」,只會建立週期工作;選 Trigger: state 也不會自動取代你對 null、unknown 與 unavailable 的失敗政策。

安全檢視停用範例

  1. 下載後先以純文字檢查。

    開啟 04-events-state.json,確認只有一個 Events: state 與 Debug;HA 節點含 "d": true,代表匯入後仍停用。這一步不執行 Node-RED 或 HA 操作。

  2. 核對固定版本欄位。

    確認 type 是 server-state-changed、version 是 6,Server 為 PLACEHOLDER_HA_SERVER_CONFIG,entity 為 PLACEHOLDER_ENTITY_ID。不要用搜尋取代填入正式環境 ID。

  3. 先畫出輸出契約。

    在紙面記錄 msg.payload 為字串 state、msg.data 為 event data、msg.topic 為觸發 entity ID。Debug 目前只看 payload,避免完整 attributes 暴露位置、人物或裝置資訊。

  4. 逐項決定異常政策。

    分別決定 old_state 為 null,以及舊/新 state 為 unknown 或 unavailable 時的路由。不要用一個「有值」判斷混在一起;entity 刪除則需另選能觀察原始事件的入口,不能期待本版 Events: state 下游收到。

  5. 檢查啟動與時間條件。

    範例的 Output on Connect 為 false、only-change 為 true、For 為 0。若未來要改,先依本章邊界模擬 reconnect 與 timer 取消情況,不要在生產事件上直接試。

  6. 完成審查後仍保持副作用斷開。

    即使之後由你在隔離環境進行測試,也只接有限欄位 Debug 或無外部 I/O 的驗證節點;不得接Action、通知、HTTP、MQTT 或檔案寫入。先保存停用狀態與審查紀錄,再由現場變更流程決定後續。

安全測試資料:用紙面事件或單元測試物件表示 entity_id: "PLACEHOLDER_ENTITY_ID",old/new 只放必要 state 與假時間。不要從 Debug 複製真實 HA event;完整 attributes 可能包含人名、位置、媒體或門鎖資訊。

離線三列練習:Events: state 節點保持停用且不連 HA,Action 全程斷開;只在紙面判讀下列假事件與預期結果。

假事件紙面設定預期輸出/不動作
PLACEHOLDER_STATE_A → PLACEHOLDER_STATE_Bentity_id: "PLACEHOLDER_ENTITY_ID";only-change=true有限 Debug 收到 msg.payload="PLACEHOLDER_STATE_B" 與 placeholder topic;Action 仍斷開
state 維持 PLACEHOLDER_STATE_A,attribute 由 PLACEHOLDER_ATTRIBUTE_A 改為 PLACEHOLDER_ATTRIBUTE_Bonly-change=true不輸出、不動作;若需求是屬性監看,先另寫明確契約
新 state 為 unknown 或 unavailable依紙面政策設定對應 ignore/異常分支忽略時無輸出;未忽略時只進 NO_ACTION_INVALID_STATE Debug,不接 Action

entity selector 與 old/new 模型

v6 的 entities 是三組 selector:entity 做 exact、substring 做片段比對、regex 做正規表示式。只有 exact 且其他兩組空白時,runtime 會為每個 entity 建立較具體的事件 topic listener;只要使用 substring 或 regex,就訂閱一般 state_changed topic,再逐事件用 shouldIncludeEvent 判斷。三組之間是「任一符合」即可,不是全部同時符合。

selector適用情境主要風險安全做法
exact entity已核准的單一或少量 IDID 遷移後漏接首選;用 PLACEHOLDER_ENTITY_ID 文件化允許清單(allowlist)
substring命名規則穩定的一群 entity同名片段誤納新增 entity列出預期集合並對新增項目 fail closed
regex確實需要結構化集合範圍過寬、效能與誤匹配加起訖錨點,在假 ID 集合離線測試

Home Assistant 原始 event 的 event.old_state 與 event.new_state 是 state object 或 null:entity 新建常見 old_state 不存在,entity 刪除則 new_state 不存在。這和 state object 存在但其 state 字串是 unknown/unavailable 完全不同。然而 0.80.3 的 WebSocket 處理會在 new_state 物件或 entity ID 為 falsy 時先丟棄該筆 state_changed,之後才發送套件事件 topic;所以 Events: state v6 收不到原始的 new_state: null 刪除事件,也不能提供下游刪除分支保證。

不要假設 state_changed 一定是 state 字串改變。attributes 或時間欄位改變也可能產生事件;only-change 開啟時,controller 比較轉型後的 old/new state,相同便不輸出一般事件。因此若你的需求是監看 attributes,不能同時假設 only-change 仍會傳出 attribute-only 更新。

state 字串、轉型與 Output Properties

HA state 原始值是字串。v6 的預設 stateType 是 str;若使用非字串轉型,controller 會在 old/new entity 上保存 original_state,再改寫 state。數字使用 parseFloat;一般 boolean 是 JavaScript 的 !!value,因此任何非空字串都為 true,連 "off" 也不例外。這就是為什麼狀態判斷優先保留字串,或使用明確的 HA boolean 規則,而不是把任意 state 強轉 boolean。

範例映射來源值缺值與隱私邊界
msg.payload ← entity state(string)通過套件前置篩選後 new entity 的 stateunknown/unavailable 仍是字串;刪除事件不會抵達此節點
msg.data ← event dataold_state、new_state 等 event data資料量大且含 attributes;只在隔離 Debug 暫看
msg.topic ← entity IDcontroller 的 triggerId屬環境資訊;公開紀錄改成 placeholder
自訂 msg/flow/global propertyentity state、event data、config、JSONata 等寫入 context 會延長保存;先最小化與定 retention

Output Properties 是明確映射,不是固定保證「所有東西都在 payload」。範例採三個預設映射;若你刪除或改名,下游也必須同步更新契約。下游只應依明確映射與已驗證的欄位運作,不把物件共享方式視為穩定契約。

0.80.3 版本細節:預設的 event-data → msg.data 映射直接保存同一個 eventData object 參照;controller 之後在送出前加入 new_state.timeSinceChangedMs,因此這個預設映射會看見該欄位。下游若需要它,應明確驗證存在與型別,不要假設後續版本仍共享物件。

若設定 If State,節點會有兩個輸出:成立走第一個,不成立走第二個;沒有條件時通常只有一個輸出。第二輸出不是 runtime error,而是條件 false。真正的設定/輸入錯誤應另由 Catch/Status 觀察,概念可銜接第 18 章。

ignore、only-change、If State 與 For

五個 ignore 選項在轉型之前判斷已抵達節點的事件:舊 state 不存在、舊 state 為 unknown、舊 state 為 unavailable、新 state 為 unknown、新 state 為 unavailable。它們只決定是否整筆略過。原始 new_state: null 確實不等於 unavailable,但 0.80.3 已在 Events: state 上游的 WebSocket 處理丟棄它,不能用這五個選項或下游分支補回。

outputOnlyOnStateChange 對一般事件比較 old/new state;若相等便返回。但 Output on Connect 產生的合成事件以 runAll=true 呼叫,會略過這個 only-change return,所以開啟 Output on Connect 時仍可能為每個符合 selector 的 cached entity 產生初始輸出。這是啟動洪峰的主要來源。

If State 比較的是轉型後的新 state,支援 is、is not、大小比較、includes/not in 與 JSONata。未設定 If State 時,不要把第一輸出稱為「條件成功」;它只是正常輸出。設定 If State 後,false 訊息仍會從第二輸出送出,不會自動被丟棄;第二輸出若接到副作用節點同樣危險。

For 的值可來自 number、JSONata、flow 或 global,必須是非負數;空字串或 0 代表不等待。有效 timer 以 entity ID 分開追蹤。一般事件開始 timer 後,若同 entity 出現不符合 If State 的事件,會把 active 標記關閉並走 false;新的有效事件會清除既有 timeout 再啟動。這是記憶體內 timer,不是 HA 歷史查詢,重啟不能當作持久倒數。

For 不是事後驗證:timeout 到時輸出的仍是開始計時時 clone 的 eventMessage;controller 沒有在到期時重新查一次目前 state。若你的風險模型要求「到期當下仍符合」,請在下游用Current State做第二次唯讀查詢,先處理 cache 斷線與缺值,再考慮任何副作用。

startup、reconnect 與安全測試邊界

Output on Connect 開啟後,listener 啟動時若 HA 已 running,會把目前 cache 轉成合成 state_changed;若尚未 ready,則等待 InitialConnectionReady 再產生。每筆合成事件的 old_state 與 new_state 都指向同一個目前 entity,並以 runAll=true 處理。它不是一段真實歷史,也不能告訴你在斷線期間發生過哪些中間變化。

runAll=true 不啟動 For timer;controller 在有效 timer 路徑會直接 return。若 Output on Connect 與 For 同時開啟,不能假設連線後每個目前符合狀態的 entity 都會等滿 For 再輸出。這個組合必須依 v6 程式行為審查,而不是依欄位名稱猜測。

controller 還要求 homeAssistant.isHomeAssistantRunning;連上 WebSocket 但 HA 尚未 running 時不處理正常事件。重新連線後 cache 是快照,事件流也不保證補齊斷線區間。因此安全自動化要採 fail closed:斷線期間不推論、重連先查目前狀態、需要歷史證據時使用受限的 Get History,而不是從兩筆事件猜中間過程。

安全測試至少涵蓋:state 真正改變、只改 attribute、old_state null、old/new 分別 unknown/unavailable、Output on Connect、短暫斷線重連、For 計時中反轉;另以 WebSocket 邊界測試確認原始 new_state: null 在進入 Events: state 前被丟棄,而不是期待節點輸出。測試只用停用 flow 與 placeholder 的紙面/單元事件;不要為了製造事件去切換真實燈、鎖、警報、空調或通知。

需要「條件成立後才觸發」可評估 Trigger: state;需要週期取樣才評估 Poll State;需要到期當下重查則接 Current State。三者都不會自動提供 exactly-once 保證。任何接到 Action 的設計,必須另加 idempotency、冷卻、重新連線政策與人工復原,詳見第 11 章。

故障排除

  • 完全沒有輸出:先確認節點仍停用是預期狀態;若在另行核准的隔離測試中,依序核對 v6、Server ready、exact/substring/regex selector、五個 ignore 與 If State。不要先放寬 regex 或接真實副作用驗證。
  • attribute 改變沒有訊息:檢查 only-change 是否為 true;它比較 old/new state,相同便略過一般事件。若需求真的是 attributes,建立明確 attribute 允許清單,避免 Debug 整個 event。
  • 啟動後突然很多訊息:檢查 Output on Connect。合成事件會遍歷 cache 並可略過 only-change;關閉前先確認下游是否依賴初始快照,任何副作用支線先斷開。
  • unknown 被當成缺值:unknown 是 state 字串;null 是 state object 不存在。分開記錄是哪一種情況,並檢查相對應 ignore checkbox,不用 truthy 判斷混合。
  • For 到期後狀態其實已改:For 輸出保留起始事件,不是到期重查。先用 Current State 做唯讀確認並處理 cache missing;未確認前保持後續 Action 停用。
  • 數字或 boolean 判斷異常:檢查 stateType 與 original_state。parseFloat 可能產生 NaN,一般 !! 會把非空的 off 字串視為 true;優先回到字串與明確比較。
  • 重連後以為漏掉事件:事件訂閱不等於歷史補送。記錄斷線窗口,若業務需要則以受限時間窗查 Get History;不要根據重連快照補造 Action。

固定來源

可下載的安全範例是 04-events-state.json。它使用 placeholder Server/entity 且 HA 節點停用,只供匯入前檢查 v6 欄位,不代表已核准在你的環境執行。

常見問題

unknown、unavailable 與 null 是同一件事嗎?
不是。前兩者是 state object 內的字串狀態;null 表示原始 old_state 或 new_state 物件不存在。v6 能分開忽略舊 state null 與 old/new unknown/unavailable;但 0.80.3 在 WebSocket 邊界先丟棄 new_state null,Events: state 下游不會收到刪除分支。
only-change 開啟後,Output on Connect 一定不輸出嗎?
不是。初始合成事件以 runAll 處理,only-change 的相等檢查只套用在 runAll=false 的一般事件。因此仍要估算 cache 中符合 selector 的數量。
For 到期時會再確認目前狀態嗎?
不會。v6 timeout 使用開始計時時的 clone event。若到期狀態是安全決策的一部分,另接 Current State 唯讀重查,並對 cache missing 採 fail closed。
為什麼不要把 state 直接轉 boolean?
一般 boolean 轉型使用 JavaScript truthiness,任何非空字串都為 true,所以 off 也會成為 true。保留字串並明確比較,或使用經審查的 HA boolean 規則。
Events: state 可以補回斷線期間的每次改變嗎?
不可以。重連 cache/初始輸出不是歷史重播。需要歷史時以有限時間窗與結果量使用 Get History,且不得由不完整資料補造副作用。
範例可以直接接 Action 測試嗎?
不可以。範例刻意停用且使用 placeholder。先離線檢查、只接有限 Debug;Action 的 target、data、queue、response 與錯誤契約必須另依第 11 章審查。