@bluedynamic/node-red-contrib-geyser-modular
v0.0.47
Published
Geyser Modular Palette - Device → Logic → Functions nodes for Node-RED
Maintainers
Readme
@bluedynamic/node-red-contrib-geyser-modular
Geyser Modular Palette – Device → Logic → Functions nodes for Node-RED.
Disclaimer: This is a personal / pet project. Not officially supported. Use at your own risk. No warranty; use of this software is entirely at the user’s responsibility.
This folder (and its contents) is the palette package. It is published to Git and, optionally, to npm.
Docs layout: Behaviour, MQTT layout, and message contracts are documented here (Node properties below) and in VARIABLE-MAP.md. In Node-RED, each node’s edit dialog only shows short tips so the UI stays readable; open this README on Git (palette repo) for the full reference.
Installation
From npm (if published): In Node-RED → Manage palette → Install → search for @bluedynamic/node-red-contrib-geyser-modular.
From a .tgz file: In Node-RED → Manage palette → Install → Install from file and select the .tgz. You can build the .tgz yourself with npm pack in this folder, or use a built file from the NodeRED-palettes share.
How "Update" works in Manage palette
Node-RED checks the npm registry. So:
- Publish this package to npm (see Releases below).
- Users install once via Manage palette → Install → search for
@bluedynamic/node-red-contrib-geyser-modular. - When a new version is published to npm, users open Manage palette, find the package, and click Update.
Releases (GitHub + npm)
- GitHub: Create a tag (e.g.
v0.0.2) and push. The repo’s workflow builds the palette and attaches the.tgzto a GitHub Release. - npm (for Update): In the repo Settings → Secrets, add
NPM_TOKEN(from npm access tokens). Then each tag push can publish to npm so "Manage palette → Update" sees new versions. - Bump
versioninpackage.jsonbefore tagging (e.g.0.0.2then tagv0.0.2).
Nodes
Stable (intended long-term)
Rename: The hardware node was
device-geyserwise; it is nowdevice-geyserwiseESP(Geyserwise-on-ESP). Existing flows will show an unknown node until you delete the old node, add device-geyserwiseESP, and re-wire (same properties: Name, Device ID, MQTT, GW ID, load, priority).
- device-geyserwiseESP – Normalizes Geyserwise-on-ESP MQTT data (3 outputs:
device_id, full event, spare). - device-logic – Single-node port of G1-Logic110: reads/writes
global.bluedyn[device_id](min temp, boost, solar, timer, load request, heating method, boost reset,set_powerfromload_allow). Trigger from output 1 of device-geyserwiseESP (msg.payload = device_id). - coordinator – Multi-device load allocator: 1 input, 3 outputs. Routes by
msg.topic(device_id,batt_soc,consumption); writessettings.load_allowper device. - device-home-assistant – 1 in / 3 out. Device ID in config (recommended), 12 entity helpers, optional MQTT broker. O2 = timer snapshot (debug). Writes
logic.timer1_* … timer3_*andlogic.timer_status. - bluedyn-mqtt-sync – MQTT
prefix/#↔global.bluedyn(same broker config as core mqtt in/out / device-geyserwiseESP). QoS 1; leaf publishes retained (MQTT has no bulk read — retain is how late subscribers catch last values); inbound{ v, t }, JSON, or plain strings; optional Clock check onprefix/_mqtt/time.
Temporary / scaffolding (remove or replace later)
- bluedyn-get / bluedyn-set – Thin read/write helpers for
global.bluedyn(GGV / SGV). Provisional: once the final logic/device node exists, this behaviour is expected to move inside that node (e.g. shared helpers / internal code paths), not stay as separate palette nodes. - test-gwise – Hard-coded MQTT subscription for bench/debug only; delete when no longer needed.
Palette look: All nodes use the same fill #3936e0 (default Node-RED label contrast, no custom editor CSS). device-geyserwiseESP / test-gwise use geyserwise.svg; bluedyn-mqtt-sync uses font-awesome/fa-exchange.
Node properties
Configurable fields in the editor, plus runtime behaviour (messages, MQTT). Internal constants (QoS, retain, TZ label, skew threshold) are described where they apply.
Temporary nodes (bluedyn-get, bluedyn-set, test-gwise) are documented below for current flows, but are not the long-term shape of the palette—see the Nodes list above.
device-geyserwiseESP
Editor order: Name → Device ID → MQTT broker → GW ID → Load size → Load priority → output tips (each output on its own line in the dialog).
| Property | Description |
|----------|-------------|
| Name | Display name; also written to bluedyn[<device_id>].settings.device_name when set. |
| Device ID | Logical id (e.g. geyser1). Key under global.bluedyn[<device_id>]. Required. |
| MQTT broker | Same shared MQTT config type as core mqtt in / mqtt out (and bluedyn-mqtt-sync). |
| GW ID | Geyserwise-on-ESP MQTT topic prefix (e.g. geyserwise-tse-f6ee1a). Subscribes to GW_ID/#. Required. |
| Load size (W) | Optional; pushed into settings.load_size. |
| Load priority | Optional; pushed into settings.load_priority. |
Flows: MQTT subscription only (no node input). On each inbound hardware event it updates global.bluedyn (hardware bucket GeyserwiseESP + logic/system) and sends 3 outputs: O1 = msg.payload + msg.device_id = device id; O2 = full normalized object (device_id, device_type = geyserwiseESP, type, payload, mqtt_topic, …); O3 = unused.
device-logic
| Property | Description |
|----------|-------------|
| Name | Display label. |
| solar_soc_start | SoC (%) to assert solar SoC gate (default 98). Clamped 0–100. |
| solar_soc_hysteresis | SoC hysteresis for solar gate; clear threshold is solar_soc_start - solar_soc_hysteresis (default 5). Clamped 0–100 and capped at solar_soc_start so the stop threshold never goes below 0%. |
| water_temp_variance | Shared water-temp hysteresis delta used in min/boost/solar switch logic (default 5). Clamped ≥ 0. |
Behaviour (G1-Logic110): Reads logic.* / settings.load_allow / power.batt_soc under global.bluedyn[device_id]. Applies hysteresis: min temp (min_temp / min_temp+water_temp_variance), boost tank band (boost_temp-water_temp_variance / boost_temp), solar SoC (solar_soc_start / solar_soc_start-solar_soc_hysteresis) and tank vs max_temp (max_temp-water_temp_variance / max_temp). Computes force_heat_status (boost temp OK ∧ boost_state === heat), solar_heat_status, and 4-way AND → settings.load_request. Writes logic.heating_*, logic.heating_method (priority: Solar Dump → Boost Heat → Timer → Min Temp → Idle), optional boost reset (boost_state / climate_mode → auto), and logic.set_power from settings.load_allow.
Outputs: O1 = msg.payload = device_id on every evaluation (route/trigger pulse). O2 / O3 only when load_request or heating_method changes (RBE); global.bluedyn is updated on every evaluation regardless.
coordinator
| Property | Description |
|----------|-------------|
| Name | Display label (default coordinator). |
| max_load | Max allocatable load budget (W). |
| reserve | Reserved headroom subtracted from max load (W). Default 500. |
| enabled | Master enable/disable for allocations. |
| delay | Debounce delay before applying logic (seconds). Default 30 in the editor. |
Input (single): Route with msg.topic — device_id + string payload registers a device (or empty topic + string payload, or msg.device_id, for direct wire from hardware O1). batt_soc + numeric payload = SoC %. consumption + numeric payload = consumption (W). Use a core Change node (or Function) upstream to set msg.topic for Victron/HA feeds. Any other message is ignored (no outputs fired for that message).
Defaults: Until first valid telemetry is received, coordinator uses batt_soc = 50 and consumption = 0. Status shows a default-warning suffix (default soc, default cons, or default soc+cons).
Outputs: O1 devices/check summary, O2 logic summary, O3 per-device output list (load_request, load_size, load_allow). Node status shows CHECK ... | LOGIC ....
device-home-assistant
| Property | Description |
|----------|-------------|
| Name | Display label. |
| Device ID | e.g. geyser1 — target global.bluedyn[<device_id>].logic. Recommended so HA works before device-logic runs. |
| MQTT broker (Home Assistant) | Optional. Same broker config type as device-geyserwiseESP / core mqtt. If set, subscribes to Subscribe topic (default homeassistant/#, QoS 1). If empty, use the input only (e.g. server-state-changed). |
| Subscribe topic | MQTT wildcard for HA state topics. Payload + topic are converted to entity_id (e.g. homeassistant/input_datetime/foo/state → input_datetime.foo). |
| Timer 1–3: start, end, temp, status | Optional full HA entity_id each (exact string). Empty = that slot ignored. |
Resolve order for device id: config Device ID → msg.device_id → stashed id from device-logic / device-geyserwiseESP O1 (msg.payload string). If still empty, entity updates warn and skip.
Status: HA MQTT line (subscribed / connected when exposed by broker) + count of matched timer-helper messages.
Slot → logic mapping (fixed):
| Config field | Writes to | Coercion |
|--------------|-----------|----------|
| entity_timer1_start … entity_timer3_start | logic.timer1_start … logic.timer3_start | string (e.g. 05:00:00) |
| entity_timer1_end … entity_timer3_end | logic.timer1_end … logic.timer3_end | string |
| entity_timer1_temp … entity_timer3_temp | logic.timer1_temp … logic.timer3_temp | number |
| entity_timer1_status … entity_timer3_status | logic.timer1_status … logic.timer3_status | bool (on/off, etc.) |
After every successful write, logic.timer_status is set to true if any of timer1_status, timer2_status, or timer3_status is on, else false — so device-logic can keep using a single aggregate timer gate.
Input: events: state / server-state-changed / Link-in, or MQTT subscription. msg.entity_id must exactly equal the configured string.
Value: Uses msg.payload, else msg.data.new_state.state, else msg.new_state.state.
Outputs: O1 — msg.payload + msg.device_id = device id (after each successful helper write). O2 — timer snapshot (entity_id, field, value, nested payload with all timers + timer_status, mqtt_topic if from MQTT). O3 reserved. Debug must use the middle output. No O2 until a configured entity matches and device id is resolved — reinstall palette + deploy if you added O2 recently.
bluedyn-mqtt-sync
| Property | Description |
|----------|-------------|
| Name | Optional label. |
| MQTT broker | Shared broker config; must support subscribe + publish like the standard MQTT broker node. |
| Topic prefix | Base MQTT topic (default bluedyn). Subscribe: prefix/#. Publishes under prefix/.... |
| Sync interval (s) | Periodic outbound scan. 0 = timer off; use the node input (e.g. inject) to trigger a push. |
| Suppress TX after RX (ms) | After an inbound message on a path, skip outbound publish on that path for this many ms (reduces echo loops). |
| Clock check | Off — does not use prefix/_mqtt/time at all; MQTT ↔ bluedyn sync works as usual (typical for a single machine or when you don’t care about clock sanity). Host — periodically publishes { epochMs, tz } to prefix/_mqtt/time (TZ is fixed in code). Client — subscribes to that topic and compares host epochMs to Date.now(); if skew > 10 s, node.error + red status (NTP issue). |
| Time publish interval (s) | Host only: how often to publish prefix/_mqtt/time (minimum 10 s in the UI). |
| Retain sync publishes | If enabled, each leaf publish uses MQTT retain. New subscribers (e.g. GX client after deploy) then receive the last value per topic as soon as they subscribe — no need to wait for the next live change. Trade-off: broker holds last message per topic (usually what you want for state mirrors). |
| Full push after deploy | Use on the machine that is source-of-truth for global.bluedyn (host). Once ~2 s after deploy, republishes every leaf, ignoring the change-only cache, so all current values hit the broker in one burst. |
| Full push every (s) | Host only. If > 0, repeats a full republish on that interval so clients that connected earlier can still receive a refresh without waiting for each key to change. 0 = off. |
| Input msg.republishFull | Send msg.republishFull = true into the node (e.g. inject) to run one full push on demand (same as deploy option, without waiting). |
Fixed in code (not in editor): sync uses QoS 1. Clock-check time topic uses retain off. Optional retain applies only to normal sync leaf payloads when the checkbox is on. Time JSON tz is Africa/Johannesburg. Client clock skew > 10 s → node.error + red status.
MQTT reality (no “download all topics”): Standard MQTT has no request for “everything on prefix/#”. A subscriber only gets (a) live publishes after it subscribes, and (b) retained messages the broker stored per topic. So cold start on a client needs either retain on those publishes, or the host to republish the full state (deploy push, interval, or inject). After that, change-only sync is fine.
MQTT: Inbound: { v, t }, JSON, or plain string/number; merged into global.bluedyn and bluedyn._updated[path]. Topics under _mqtt/ (except _mqtt/time for clock check), snapshot, and leading _ are ignored for normal sync. Outbound: one topic per leaf; by default change-only vs last sent value — tx 0 is normal on a client that only receives and doesn’t alter bluedyn. Time topic: { epochMs, tz }, not stored in bluedyn.
Status line: MQTT disconnected (red) if the broker config is not connected; otherwise MQTT OK · … with subscribe / rx … / tx N / clock skew text. When there is nothing to publish, the node does not keep resetting the line to grey tx 0 (that hid the last rx … and looked like a drop-out). One message at a time on the status is normal: MQTT delivers topics individually, and each inbound update applies to global.bluedyn as it arrives — not a batch snapshot.
Troubleshooting
- Old editor fields (QoS, Retain, TZ, skew ms): Not in the current node template. If they still appear, Node-RED is loading an older copy of the package — reinstall from a fresh build of this repo, restart Node-RED, redeploy.
- Client
global.bluedynalmost empty but MQTT Explorer shows data: Use a build with plain-string inbound support; verify topic prefix and broker. Explorer can show traffic that was never retained and happened before the client subscribed — MQTT won’t replay that. This node retains what it publishes; if HA (or another writer) publishes the same tree without retain, fix retain on that side or ensure a live republish after the client connects. - Client on LAN / clock check: On the host set Clock check → Host; on the client → Client; same broker and prefix. Not required for sync itself — only for detecting clock skew. Red skew status is not overwritten by routine
tx/rxlines. MQTT disconnectedunless anmqtt inis also on the flow: Node-RED’s mqtt-broker only opens the TCP connection when at least one node callsregister()(as mqtt in / mqtt out do). Palette nodes now register the same way, so bluedyn-mqtt-sync / device-geyserwiseESP can connect without a dummy mqtt in.
bluedyn-set (Set Global Var) — TEMPORARY
Provisional node. Exposes SGV-style writes for early flows. When the final node is implemented, this logic should live inside that node (or shared module), and this palette node may be removed.
| Property | Description | |----------|-------------| | Name | Optional label. |
Input: msg.type (string key) and msg.payload (required; must not be undefined). Optional msg.device_id overrides flow context.
Outputs: 1 = success (original msg forwarded), 2 = error (msg.error set).
Writes: If msg.type === flow.hardware (flow context hardware), writes bluedyn[device_id][hardwareKey] = payload (hardware blob). Otherwise msg.type must be a known key: writes to bluedyn.power[key] or bluedyn[device_id][folder][key] according to the internal key→folder map (see bluedyn-var.js / VARIABLE-MAP.md). Requires flow.device_id for non-power keys.
bluedyn-get (Get Global Var) — TEMPORARY
Provisional node. Exposes GGV-style reads for early flows. When the final node is implemented, this logic should live inside that node (or shared module), and this palette node may be removed.
| Property | Description | |----------|-------------| | Name | Optional label. |
Input: msg.type = key to read.
Read order: bluedyn.power[key] first (shared). Else, with flow.device_id, search settings → system → logic under that device. If the key contains /, only bluedyn[device_id][flow.hardware][key] is used (no folder fallback). Uses flow.device_id and flow.hardware from flow context.
Not found: Red status; no output message (same idea as return null in a function node).
test-gwise — TEMPORARY
Bench / debug only. Not part of the final product; safe to drop from the palette once you no longer need a fixed-topic MQTT sniffer.
| Property | Description | |----------|-------------| | Name | Label. | | MQTT broker | Broker for a single hard-coded test subscription. |
Subscribes to geyserwise-tse-f6ee1a/sensor/water_temperature/state and shows payload in status.
Choosing icons (official references)
- Font Awesome 4.7 (names like
fa-exchange,fa-refresh): fontawesome.com/v4.7.0/icons — in code:icon: "font-awesome/fa-exchange". - Stock Node-RED SVGs (e.g.
bridge.svg,arrow-in.svg): Creating nodes → Appearance — useicon: "arrow-in.svg"etc. - Custom SVG: white on transparent, ~2:3, min ~40×60 — place under
src/icons/or next to the node;icon: "yourfile.svg".
Architecture
Device Nodes → Logic Nodes → Function Nodes.
See docs/ARCHITECTURE.md in the parent folder for the full design.
