Nothing you store remembers on its own, and one connection is already built for you
Everything so far has lived inside a single message as it crossed the canvas. This part introduces the first state that outlives a message — a countdown that keeps running after the wire that started it has gone quiet, a counter that has to mean the same thing the next time a different message asks for it. It also introduces the thing every later part in this guide leans on: the map of the 32 Home Assistant node types your add-on already ships, and the one connection among them you are not supposed to build yourself. Get both wrong and the failure looks the same — something that seemed to work now quietly does not, on a restart you did not even think to test.
“It worked yesterday” is not the same claim as “it survives a restart”
Take a request as ordinary as “turn off the light two minutes after motion stops.” That single sentence quietly asks for timing, for handling a second motion event before the two minutes are up, for canceling the countdown if the room is occupied again, and for deciding what happens if Node-RED itself restarts halfway through the wait. “How many notifications went out today?” asks a smaller but just as easy to get wrong question: which part of the flow owns that number, and does it still mean anything after the add-on comes back up?
This part builds neither light nor notification. Every example below ends at a Debug node, the same discipline Part 2 established, because the two subjects here — timers and state — are exactly the ones people get burned by once they are wired to something that actually flips a switch. Later parts move this into real Home Assistant flows once the underlying behavior is second nature.
localfilesystem context store does not automatically preserve a timer that is counting down inside a node — the countdown lives in the node's memory, not in the store, no matter what you have configured.The second half of this part turns to a different kind of “it looks done and is not”: the 32 node types your Home Assistant integration ships with. The add-on already has a working connection to Home Assistant before you drag a single node onto the canvas. The most common way to break that connection is to try to be helpful and build a second one.
A Delay queue and a Trigger countdown are both more like a kettle left on the stove than a slow cooker with a timer built in. Walk away and it keeps going for a while, on its own heat. Cut the power — a restart — and there is no plug-back-in that resumes where it left off. Whatever was mid-boil is just gone, and the pan starts cold the next time you turn the stove on.
Queueing and resetting are different jobs, and one node is not secretly the other
In the Node-RED 5.0.2 runtime, Delay supports a fixed delay, a dynamic delay overridden by msg.delay, a random delay, and several rate-limiting paths. In delay mode, a message carrying msg.reset clears whatever is waiting in the queue, and one carrying msg.flush releases it early. Rate-limit mode can queue messages or drop them, and it is bounded by the runtime's nodeMaxMessageBufferLength setting. Treat all four of those — delay, rate, reset, flush — as control inputs: do not let untrusted input set any of them freely.
Trigger, in the same 5.0.2 build, can run one shared timer for every message or keep a separate timer per value of a chosen message property, most often msg.topic. With extend enabled, a new message restarts the wait instead of queuing behind it; msg.reset cancels the matching timer outright. That behavior is closer to “only send a turn-off candidate after the last motion event has gone quiet” — and it is still only a candidate. Query the entity's current state and check occupancy again before you act on it. A message that is two minutes old is not a current fact.
| Requirement | Better fit | Required safeguard |
|---|---|---|
| Delay every message by the same interval | Delay | Limit queue length and input rate |
| Control the rate of API calls | Delay, rate limit | Timeout, an error branch, and a queue policy |
| Output only after the last message | Trigger, with extend | Separate timers by topic, and provide a reset path |
| Send a start value now and an end value later | Trigger | Define repeated-input and restart behavior up front |
flowchart TD A["A new message reaches
a Delay or a Trigger node"] --> B{"Which one, and
which mode?"} B -->|"Delay, queue"| C["The message waits.
msg.flush releases it early,
msg.reset clears the queue"] B -->|"Delay, rate limit, drop"| D["Only the newest message
survives. Older ones in the
same window are discarded"] B -->|"Trigger, extend on"| E["The countdown restarts from
zero. msg.reset cancels it,
with no end output at all"]
msg.topic, that set of topics has to stay bounded. A source that keeps generating new topic values — broken code, or something hostile — leaves Trigger's topic map accumulating pending timers forever. Put a Switch allowlist in front, or map anything unrecognized to one fixed quarantine topic, before it reaches the node.Pick the narrowest scope, because the wider ones do not tell you who else is using them
Node-RED gives you three places to keep a value between messages, and the question that chooses between them is always the same: which nodes actually need to read this? node context belongs to one node instance — use it for that node's own counter or previous value. flow context is shared by every node on the same tab — use it to coordinate one automation. global context reaches every flow in the whole runtime, which is exactly why it should hold only data that truly has to cross flows and has one clear owner.
A fourth term sits next to those three and answers a different question. Scope answers “who can read this value?” A context store answers “where does it actually live?” Do not conflate the in-memory default with a named localfilesystem store — the next section is entirely about that gap.
flowchart TD
subgraph GLOBAL["Global scope, reachable from every flow"]
G["global.get() / global.set()"]
end
subgraph TABA["Flow tab A"]
A1["Node 1, its own node context"]
A2["Node 2, its own node context"]
AF["Flow context for tab A,
shared by nodes on this tab"]
A1 --> AF
A2 --> AF
end
subgraph TABB["Flow tab B"]
B1["Node 3, its own node context"]
BF["Flow context for tab B,
a separate value from tab A"]
B1 --> BF
end
AF --> G
BF --> G
| State | Recommended scope | Why | Limit or cleanup |
|---|---|---|---|
| A node's consecutive error count | node | No sharing required | Cap it, and reset to zero on recovery |
| A notification-suppression flag for one room | flow | Several nodes coordinate | A boolean or small timestamp, with an explicit reset |
| Site-wide maintenance mode | global, used cautiously | Multiple flows need to read it | One clear owner, a safe default, and a change log |
| Complete sensor history | Not context at all | Unbounded, and queried differently every time | A managed data system with its own retention policy |
node context is a sticky note on your own desk — nobody else even knows it is there. flow context is the whiteboard in your team's own room — anyone on that team reads and writes it, and a different team down the hall has never seen it. global context is the noticeboard in the building lobby: every team walks past it, which is exactly why you do not pin your team's rough draft there. The wider the board, the more people can quietly change what it says.
Name a context key by what it is for, not by what it happens to describe — notificationCount or maintenanceMode, not data or temp, and never an address, a username, or a device ID folded into the key itself. Deleting a flow or moving a node can change the identity behind its node or flow scope, so rerun your initialization tests after any upgrade or refactor. A Change node's typed input, or the Function API, can also name a specific store: if that store does not exist, the runtime logs an unknown-store warning and quietly falls back to the default store. That fallback is not successful persistence — check the runtime log and the actual restart behavior before you trust it.
Memory forgets on purpose. localfilesystem remembers on a delay, not instantly
When no contextStorage plugin is configured, the Node-RED 5.0.2 runtime uses the memory store by default. That is fast, and entirely appropriate for a value that only needs to last a few messages — but it is not expected to survive a process restart. Getting localfilesystem means configuring it explicitly in the runtime settings, and this part does not walk you through touching your add-on's shared settings directly: a mistake there affects every flow at once, so back up first and make that particular change during a maintenance window.
localfilesystem keeps its own memory cache turned on by default, and its documented default flushInterval is 30 seconds — the minimum gap between writes to storage, there to reduce wear on the underlying disk. A set updates that cache immediately and marks the write as pending; a timer flushes it to disk later, and an ordinary shutdown also attempts one last flush. Nothing you write is durable the instant you write it. A sudden power loss or a process crash can lose whatever updates had not been flushed yet.
sequenceDiagram participant F as A flow, calling context.set() participant M as Memory cache participant D as Disk (localfilesystem) F->>M: first set() call M->>M: cache updated, write marked pending Note over M,D: up to flushInterval (30s default) passes M->>D: the pending write flushes to disk F->>M: a second set(), moments before a crash Note over M: the process crashes right here M--xD: this second write was still pending, and never arrives
Memory context is a note on your own hand — instantly there, and gone the moment you wash up. localfilesystem is more like a mailroom that does one pickup every 30 seconds: drop a letter in the tray and it is not actually in the postal system yet, it is just sitting in the tray waiting for the next round. Most of the time that is fine. The one moment it is not fine is a fire in the mailroom between pickups — the letter you dropped seconds ago is gone with it, and no amount of trusting the tray changes that.
Saving to disk this way is not a database transaction, and it is not a backup. Serialization logs a warning on a circular reference, and live objects, functions, and anything else unsuitable for JSON should never be stored. A full disk, a permissions error, file corruption, or a failed backup or restore can all still cause data loss on top of the flush gap. For anything safety-critical, let Home Assistant or a dedicated data service stay the authoritative source, and re-check it when the flow starts rather than trusting old context blindly.
A conceptual settings.js sketch, for review only — do not apply anything like it without a backup and a maintenance window:
| Key | Value | What it does |
|---|---|---|
default | "memoryOnly" | Names which store below is used when a node does not ask for one by name |
memoryOnly.module | "memory" | The fast, non-durable store — the runtime's own out-of-the-box default |
durable.module | "localfilesystem" | A second, named store — only values explicitly pointed at this one get flushed to disk |
Four limits every long-running flow needs
Bound four things at the input, rather than trusting host memory to catch the overflow for you: the maximum number of pending messages, the maximum number of timers or topics, the maximum size of any one context value, and the maximum lifetime a piece of state is allowed to have. Route anything over those limits to a Catch, Status, or diagnostic branch, strip the payload down, and record only the count — not a full household event log.
| Test | Procedure | Expected observation |
|---|---|---|
| Single message | Inject one message by hand | Start value, the wait, then the end value |
| Repeated message | Inject again during the wait | Whether it queues, drops, or extends the timer — on purpose, not by accident |
| Cancellation | Send a reset with a fixed topic | Only the matching timer clears, with no end output at all |
| Parallel timers | Interleave two fixed topics | Timers stay isolated, and the topic map itself stays bounded |
| Restart | Restart during a countdown, through an approved process | The pending timer is gone, not resumed; startup enters a safe state |
| Context | Restart after writing a value | Memory state disappears; a configured file store is checked against its real contract, not its name |
| Over limit | Test the limit with a small, controlled burst | A visible, deliberate rejection — never an unbounded queue |
A safe startup policy for entrance lighting, in plain terms: after a restart, do not resend an old turn-off command left over from before. Wait for the next real event, then query current occupancy and the light's current state before doing anything. A notification counter can reset at a daily boundary and carry a hard maximum, rather than climbing forever. Daylight saving time, time zones, and calendar-style scheduling belong to the Home Assistant time nodes covered later in this guide — Delay's millisecond wait is not a calendar.
Home Assistant nodes are more than a button that calls a service
The Home Assistant integration bundled with this guide's pinned add-on — 22.0.1, carrying Node-RED 5.0.2 and HA WebSocket 0.80.3 — registers 32 node types. Some receive events, some read from a cache, some call an action with a real side effect, and a handful create companion entities inside Home Assistant itself. Treat them all the same and you will eventually mistake a read for a trigger, use a shared config node as if it were a flow step, or hand untrusted input straight to a high-risk action node.
The add-on's own manifest declares homeassistant_api: true, granting it an authorization capability, and the package documentation offers add-on users a specific “I use the Home Assistant Add-on” connection mode. Together those two facts describe the preconfigured path this whole guide follows.
Confirming the connection without touching it
-
Step 1
Confirm the baseline, and back up first
Check the version on the add-on's information page before you change anything, and back up your flows and credentials. Say “add-on 22.0.1, bundling Node-RED 5.0.2,” not “Node-RED 22.0.1” — they are not the same number, and mixing them up makes troubleshooting harder for everyone, including future you.
-
Step 2
Start with a read-only node
Drop in a Current State node, or another node that only queries after a manual message. Do not reach for Action, Fire Event, API, Webhook, or a companion switch for a first connection test.
-
Step 3
Select the existing Server config — do not create one
In that node's Server field, pick only the Server config the add-on already preconfigured. If it is missing from the list, stop and go straight to the troubleshooting table below rather than adding a substitute of your own.
-
Step 4
Use a placeholder entity
If a test needs an entity, use one approved for testing in your own environment, and write
sensor.example_temperaturein any notes or documentation you keep — never the real ID. The same goes for device, area, tag, zone, and webhook IDs. -
Step 5
Deploy narrow, then validate the failure paths
Use the narrowest Deploy scope, check the node's status and the add-on log, and keep Debug limited to the fields you actually need — never the whole config, headers, or state attributes. Then, without touching credentials, test a missing entity and an unknown or unavailable state, with Catch and Status nodes confirming that a failure never reaches a node with a real side effect.
The Server editor itself carries the connection mode, a connection delay, a base URL and access token for non-add-on connections, certificate options, a heartbeat, plus settings for exposing HA booleans to global context, the autocomplete cache, and selector or status UI behavior. Do not disable certificate validation to work around a troubleshooting dead end, and do not paste a base URL, an access token, or a full config export into a public issue or a message to someone helping you.
Current State, Events: state, Action, and several other nodes all point at that one Server config, so changing it is a shared change, never one confined to a single dialog. Check its references before you touch it, and review the actual scope Node-RED reports under Modified Nodes and Flows afterward. Connection delay and heartbeat manage the connection itself — they are not application-level retries, and neither one can guarantee that a particular action will succeed.
Executable, entity, config, deprecated — and only one is not for a new flow
You do not have to memorize all 32 before you can be useful. The learning path this guide follows runs: state events first, then state queries, then the Action node for the first real side effect, then the remaining event and time nodes, and only after that the practical projects — motion lighting, notifications, and the companion entities Home Assistant sees back. What matters right now is being able to place any node you meet into one of four buckets before you wire it into anything.
flowchart TD T["HA WebSocket 0.80.3
32 NodeTypes total"] --> E["19 executable nodes
Events, Current State,
Action, and more"] T --> N["9 companion-entity nodes
Sensor, Switch, Button,
and Update Config"] T --> C["3 config nodes
Server, Device Config,
Entity Config"] T --> D["1 deprecated node
the old generic Entity"]
state_changed event.api-call-service, whatever the palette currently calls it.The palette's current label for that third example is Action; older flows and documentation still call it Call Service. Use the old name only to recognize existing material — in 0.80.3 the persisted type field in your flow JSON stays api-call-service regardless of what the editor displays. Never hand-edit an exported JSON file's type to action, and never recreate credentials just because a label changed.
Config nodes hold settings; executable nodes act on a message
A config node is the address book entry, not a stop on the delivery route. The Server config holds the address, the phone number, the way in — a dozen different nodes look it up before they act, but the address book itself never moves a single package. Action, Current State, and the rest are the actual couriers: they read the address book, then go do something with a real message in hand. Delete the address book entry and every courier who relied on it is suddenly standing in front of a door with no address to give.
A Server config is shared configuration, persisted as type server. It is not an ordinary step with an input and an output — it can sit invisible on the canvas even while more than a dozen other nodes depend on it, which is exactly why deleting an invisible setting can silently disconnect a whole group of nodes at once. Device Config and Entity Config are the other two real config nodes, holding metadata for companion devices and entities. Update Config looks similar by name but is not one of the three — it is an executable utility with one input and one output that edits that companion metadata.
| Issue | Layer to fix it in |
|---|---|
| HA connection failure, or the shared heartbeat/config | The Server config, and the add-on log |
| Wrong selector or output property on one entity | That specific executable node |
| A companion entity's name or metadata | Entity Config or Device Config, plus the entity node itself |
| Target, data, or response for one particular action | The Action node's own input contract — not the Server |
Read the current Action UI as three parts: action, target, and data. Target selects entities, devices, or areas; data supplies the parameters. What comes back depends entirely on the underlying HA action and is not guaranteed to land in msg.payload — Current State, Get Entities, Action, and several others all let you configure the output property, so never assume a node always overwrites the payload by default.
id from its input and can update name, icon, entityPicture, and options. Build the message reaching it from that allowlist alone; strip every other override before the node, and refuse anything arriving from HTTP, MQTT, or a webhook without validation first. Never wire raw msg.payload from an unverified source straight into Update Config — or, for that matter, into the API, Fire Event, or Action node, or any companion switch or button. Those are exactly the surfaces with real side effects, and Action and Current State's own Block Input Overrides setting does not generalize to every node on this list.Binary Sensor and Sensor mostly output state; Button triggers a flow when pressed inside Home Assistant; Number, Select, Text, and Time accept a constrained value; Switch carries get, set, and listen behavior all at once. Every one of those types still needs its own policy for availability, input validation, uniqueness, and what happens when it is removed or migrated — none of that is automatic just because the node exists.
All 32 node types, by category
home_assistant — 19 executable nodes
| NodeType / persisted type | Role |
|---|---|
Action / api-call-service | Calls an HA action; has a real side effect |
API / ha-api | Advanced WebSocket and HTTP API access — needs an allowlist, timeouts, and care with sensitive output |
CurrentState / api-current-state | Queries one cached entity when a message arrives |
Device / ha-device | Uses an HA device automation trigger or action definition |
EventsAll / server-events | Subscribes to the HA event bus, optionally filtered by event type |
EventsCalendar / ha-events-calendar | Fires on calendar entity event times |
EventsState / server-state-changed | Handles state_changed events and old/new state |
FireEvent / ha-fire-event | Sends an HA event; has an external side effect |
GetEntities / ha-get-entities | Filters cached entities into array, count, random, or split output |
GetHistory / api-get-history | Queries HA history; bound the time range and result size |
PollState / poll-state | Reads an entity on a schedule; prefer an event-driven design first |
RenderTemplate / api-render-template | Asks HA to render a Jinja template — a different language from Mustache or JSONata |
Sentence / ha-sentence | An Assist/conversation sentence trigger |
TriggerState / trigger-state | Combines a state trigger with conditions and constraints |
Tag / ha-tag | Receives HA tag events; de-identify the tag ID in any notes |
Time / ha-time | Schedules by a fixed or entity-derived time, day, and offset |
WaitUntil / ha-wait-until | Waits for an entity condition or a timeout; bound the number of pending waits |
Webhook / ha-webhook | Receives a webhook trigger; its URL and ID are secret entry points |
Zone / ha-zone | Detects entry and exit; location data is sensitive |
home_assistant_entities — 9 nodes
| NodeType / persisted type | Role |
|---|---|
BinarySensor / ha-binary-sensor | Exposes and updates a binary sensor in HA |
Button / ha-button | Exposes a button whose press triggers a flow — validate the source and its side effects |
Number / ha-number | Exposes a bounded number entity |
Select / ha-select | Exposes a select entity with allowlisted options |
Sensor / ha-sensor | Exposes and updates a sensor; avoid high-frequency or sensitive attributes |
Switch / ha-switch | Exposes a switch; a set operation can enter the flow and cause a side effect |
Text / ha-text | Exposes a text entity; limit input length and permitted content |
TimeEntity / ha-time-entity | Exposes a time entity — different from the executable ha-time trigger |
UpdateConfig / ha-update-config | Executable utility that updates companion metadata — restrict fields, reject untrusted overrides |
config — 3 nodes, plus 1 deprecated
| Classification | NodeType / persisted type | Role |
|---|---|---|
| config | Server / server | Shared HA connection and package settings |
| config | DeviceConfig / ha-device-config | Companion device metadata |
| config | EntityConfig / ha-entity-config | Companion entity metadata |
| deprecated | Entity / ha-entity | Legacy generic entity — kept for maintenance and migration, not for new flows |
Three numbers that all sound like the same requirement, and are not
The 0.80.3 package README lists its own user prerequisites: Home Assistant 2024.3+, Node-RED 3.1.1+, and Node.js 18.2.0+. Those last two are also verifiable straight from the package metadata. That trio is what the package itself asks of you.
The add-on's own config.yaml separately declares homeassistant: 2023.3.0 — the Supervisor's installation floor for the add-on, which does not replace the package's 2024.3+ requirement. A third number, the internal constant HA_MIN_VERSION = '2023.12' inside the package's own source, cannot lower that user-facing floor either. All three numbers live in different files, answer different questions, and need to be reported separately if you ever ask for troubleshooting help.
| Source | Value | Correct reading |
|---|---|---|
| HA WS README | HA 2024.3+ | The user-facing prerequisite for 0.80.3 |
| HA WS package.json | Node-RED ≥3.1.1, Node ≥18.2.0 | The package's own engine and host floors |
| Add-on config.yaml | HA 2023.3.0 | The Supervisor's installation floor — not a guarantee the package's features all work |
| HA WS const.ts | 2023.12 | An internal constant; it does not replace the README's number |
| Add-on package.json | Node-RED 5.0.2, HA WS 0.80.3 | This guide's own pinned baseline |
Before upgrading any one of those layers, back up first, read the release notes and migration guidance, and test the change in a flow with every side-effecting node disabled. Legacy Entity nodes, the persisted Action type, output-property settings, and unknown, unavailable, or null old/new states are the migration hazards that actually show up in practice. Do not search-and-replace a flow's exported JSON types by hand, and never treat a new palette label as if it were the persisted type underneath it.
sensor.example_temperature and strip access tokens, headers, locations, calendar contents, and state attributes before you share anything. Never copy a fictional ID into a production flow, and never let a documentation example bypass the allowlist your real environment actually enforces.Twelve symptoms, and which of them is actually a bug
| Symptom | Likely cause | What to do |
|---|---|---|
| Every motion event triggers a later action, and actions pile up | You are queueing with Delay when the job actually calls for Trigger with extend | Disconnect the side-effect output first, clear the queue, then use repeated manual inputs to verify the reset behavior before reconnecting anything |
| Trigger's reset does nothing | Timers are separated by topic, and the reset message's property does not exactly match the original | Compare the two topic values directly, and make sure unknown topics cannot silently create new timers |
| A counter returns to zero after a restart | No contextStorage is configured, so context is running on memory |
Confirm the current store. If the value truly must survive restarts, back up and review the change before configuring a named localfilesystem store, then run the restart test yourself |
| A recent file-store write is missing after a power failure | The write was sitting in the memory cache, inside the flush gap, when power was lost | This is consistent with the cache-and-flush design, not a bug. Never promise zero data loss from it; let an authoritative system reconstruct anything safety-critical instead |
| The Context sidebar shows a value a Function node cannot read | A mismatch in scope, tab, key, or named store between the two places | Check node/flow/global scope, the tab, the key, and the store name all line up, and look for an unknown-store warning in the runtime log |
| Memory or storage keeps growing | An unbounded Delay queue, Trigger topic map, pending Join, or context array | Stop the source first, then set limits and expiry, and back up any de-identified diagnostics you need before cleanup |
| The add-on's preconfigured Server config is missing | Something is wrong with the install or the connection itself, not with your flow | Stop immediately. Confirm you are in the right add-on editor, that startup and the add-on log look normal, and check the installation against the pinned add-on documentation — do not add a Server config or paste in a token to work around it |
| Only one node produces no output | Its trigger or input mode, entity selector, or output property is wrong, or it received unknown/unavailable | Isolate it with a manual Inject and a Debug node scoped to only the fields you need — do not touch the shared credentials to chase this |
| A legacy Entity node appears after an import | That is the deprecated ha-entity type |
Back the flow up, read the migration guidance, and map it to a specific companion entity by hand — never edit its JSON type directly |
You cannot find a JSON type called action |
The palette label changed; the persisted type did not | Expect api-call-service in the exported JSON — that is correct, expected compatibility behavior, not something to fix |
| A version above the stated install floor still looks incompatible | You are comparing against the wrong one of the three floors above | Do not confuse the add-on's HA 2023.3.0 floor with the package's HA 2024.3+ prerequisite. Record all five layers — add-on, Node-RED, HA WS, HA, and Node.js — when you ask for help |
| Debug output discloses an ID or an attribute it should not | Full-message Debug output was left on somewhere | Turn it off immediately, clean the sidebar, logs, and any exports, and substitute placeholders before opening a public issue. If a credential may have leaked, rotate it through your incident-response process and do not pass the value itself to anyone helping you |
The ones that come up again and again
Repeated events produce several notifications in a short window. Delay or Trigger?
Does flow.get("exampleKey") inside a Subflow read the parent tab's value?
flow.get("$parent.exampleKey"), which is how the Node-RED context API accesses the parent flow's context on purpose — and document the owner, default value, and cleanup policy when you do. Otherwise keep the value inside the Subflow and avoid the hidden coupling entirely.Does localfilesystem guarantee that no write is ever lost?
Can I keep a complete Home Assistant state object in global context?
Will a Trigger countdown resume after a restart?
Do add-on users need to create a long-lived access token?
Can Update Config double as a general way to write entity state?
id, name, icon, entityPicture, or options allowlist — reject every other override.Does every flow need its own Server config?
Which Home Assistant version floor should I actually trust?
Must a flow create a companion entity the moment it calculates a temporary value?
Can an external HTTP request update a companion entity's name directly?
id, name, icon, entityPicture, or options values and rejecting everything else.Where to go from here
Part 1 is done. Nothing you built in it could reach a light or a lock.
Every flow across these four parts stopped at a Debug node on purpose. Part 5 opens Part 2 of this guide by wiring the first node that actually listens to Home Assistant — Events: state, watching a real state_changed event go by, using the Server config this part told you not to rebuild. The scope discipline, the restart tests, and the placeholder-entity habit all travel forward unchanged; the only thing that changes from here is that a real entity is finally on the other end of the wire.
Part 4 of the WoowTech Complete Node-RED Guide series on the Apporo blog.
Adapted from the WoowTech Complete Node-RED Guide, produced by WoowTech and released under CC BY 4.0. This adaptation is published by Apporo under the same licence.
Light · Air · Water · Control · apporo