Groups get a value of their own - #43
Merged
Merged
Conversation
A group has been a write-only concept: a command target with no state, which left hal2Value, hal2Gate and hal2Bayes unable to use one at all and hal2Event offering a Group category that did not work. Give a group a Value on the Event handler's Groups tab and it gains a state computed from its members — average, any true, count true, percent true and the rest — shaped exactly like a thing item's, so every reading node can consume it the same way it consumes an item. A group without one stays exactly what it was. The aggregation lives in resources/group-aggregate.js, UMD like bayes-scale, so the functions the editor offers cannot drift from what the engine computes. It is pure: the engine decides which members count and hands over only those. What counts is the load-bearing part. A member contributes only if its item carries state and its thing is alive — a device that dropped off the network must not keep voting with the value it had before it went quiet, so `any true` goes false when the last reachable lamp disappears rather than reporting a light that may well be dark. The liveness rule is now lib/common.js:isThingAlive, shared with get_all_states so the group engine and the MCP catalog cannot disagree about what offline means. The heartbeat item is not a group member but does trigger a recompute, or a group would keep counting a device for as long as it stayed silent. Nothing eligible yields undefined rather than 0 or false, so a silent group can never read as a real "off" in a Gate. The group emits on every member update but only moves laststate/last_change when the value actually changed, which is what hal2Thing.updateState does — so hal2Event's change filters behave identically whether they watch a thing or a group. It is emitted straight onto the bus rather than through publishUpdate: a derived value has no business in the history database. Also fixes two bugs found on the way. hal2Event wiped thing/item/typeSel every time a group-configured node was opened, because it looked a group id up with RED.nodes.node() and groups are not nodes — that was the whole of "groups don't work in Events". And hal2Bayes omitted 'time' from its source whitelist, so a saved time-of-day step silently reopened as a thing step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six editor problems, one of them the reason no group appeared anywhere. halGetGroups returned an empty list whenever the Event handler id it was given did not resolve, which is exactly the case on the Value and Gate nodes: their Event handler field is new, so every existing instance carries an empty one. It now falls back to every handler in the flow — via eachConfig, because filterNodes only walks flow nodes and would never see a config node at all. A group id is unique across handlers, so the fallback cannot pick the wrong one. The Value node's dialog would not open. Its thing-change handler dereferenced RED.nodes.node(...).thingType without checking, so a select with no matching option — a new node, or a saved id absent from the current list — threw and aborted oneditprepare. Guarded, and the select no longer ends up unselected. The Value field on a group was disabled until the group had stateful members, which made it unreachable at the one moment you want it: a group is created before anything joins it. It is now always selectable and the tip reports the member count instead of gating on it. Also: an explicit "- no groups with a value -" placeholder instead of an empty dropdown, "offline members are left out" instead of the clumsier phrasing, and the tip wraps instead of running off the row, which forbids wrapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A group carried the last *different* value forward as laststate, so every subsequent member update still looked like a transition: two consecutive reports both arrived as 25.28 with laststate 25.25, and hal2Event's "on change" filter passed both. hal2Thing sets laststate to the previous value unconditionally and makes only last_change conditional (core/thing.js:218-231). Groups now do the same, so the second report reads 25.28 with laststate 25.28 and is correctly suppressed. The transition moves into group-aggregate as nextRecord() to be testable — covering the regression itself, the first computation, and a group going silent (a real change to undefined) versus staying silent (none). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Setting it to "none" passed validation even on a group source, which then fails at runtime with "no event handler configured". It is now required exactly when it is used: on hal2Value when the Category is Group, on hal2Gate when any rule reads a group. Elsewhere it stays optional, so existing nodes are not flagged. The Value node only shows the field for a group, with a line saying why one is needed at all: a Thing carries a reference to its own handler and the node reaches the bus through it, but a group is not a node — its value lives in the handler's group registry, so the owning handler has to be named. The validator prefers the live Category while the dialog is open and the saved one otherwise, keyed on an id unique to this template — hal2Event also has a #node-input-typeSel, and reading that one would judge this node by another node's open form. Both tips are now form-tips blocks indented as a whole, so a wrapped second line lines up under the first instead of running back out to the left margin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Event handler tip explained the whole ownership model where "pick the one that owns the group" says everything the field needs, and the Value tip pointed at the Action node — true, but not what you are there to find out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A form-tips block fills its parent, so margin-left indents the left edge without shrinking the box — it ran past the right edge of the edit tray by exactly the indent. Given the same width as the fields above it, calc(100% - 170px) after the 105px label column, it now sits inside the dialog with its edges aligned to the inputs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It described the selected group but sat above the field that selects it. Moved below, and given to hal2Event with the same markup and indent so a group reads the same way in both nodes. It hides rather than blanks when no group is selected — an empty tip box reads as something failing to load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A rule whose source is a group was dropped while loading the editor: the loader resolved rule.thing with RED.nodes.node(), a group id is not a node, so the row was never added to the list and the next save wrote the rule away. Same shape as the bug that wiped a group out of hal2Event. Group rules are now recognised against the group registry. While in there: the dynamic-thing branch called addItem twice, once inside the guard and once after it, so every "set by msg.thing.id" rule was duplicated each time the dialog opened. Each rule that reads a group now shows the same Value line as hal2Event and hal2Value, so what the rule is comparing against is visible where the rule is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Gate tip sat a notch right of the field above it: its labels are 80px with box-sizing:border-box, so the padding is inside that width and the column is 80, not 90. And form-tips caps itself at max-width:450px, which pulled every tip's right edge in short of the field it belongs to — lifted wherever an explicit width is set. hal2Bayes now shows the same line, last in the step so it sits under the timing row, indented to the 50px lead column the selects start in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same reading order as the other nodes: pick a group, and what it computes is stated directly beneath. Its width was auto with no box-sizing, so form-tips' 8px padding and 1px border landed outside it — 18px of overflow past the right edge, unchanged at any window width. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It used fixed margins where the rows around it are flex, so it only lined up while the remove-button column was visible. refreshRuleView hides that column for a single-step rule, and the fields grow into the space — leaving the tip stopping 34px short and resizing on a different curve. It now has the same three-part shape as the lines above: the 50px lead the selects start in, a flexible middle, and a bs-endpad the existing toggle picks up by class along with the others. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It was created after the Rule row and so rendered last, while the other three nodes carry it directly under the field it describes. Rows render in creation order, so building it right after rowName is the whole change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A group has been a write-only concept: a command target with no state. That left
hal2Value,hal2Gateandhal2Bayesunable to use one at all, andhal2Eventoffering a Groupcategory that did not work.
Give a group a Value on the Event handler's Groups tab and it gains a state computed from its
members, shaped exactly like a Thing item's — so every reading node can consume it the same way it
consumes an item. "Alla lampor → any true" is one boolean for the whole house; "Temperatur inne →
average" is one number. A group without a Value stays exactly what it was.
The functions
latest,min,max,average,median,sum,range,any true,all true,any false,all false,count true,count false,percent true.They live in
resources/group-aggregate.js, UMD likebayes-scale, so the list the editor offerscannot drift from what the engine computes. The module is pure: the engine decides which members
count and hands over only those.
What counts is the load-bearing part
A member contributes only if its item carries state and its Thing is alive. A device that has
dropped off the network must not keep voting with the value it had before it went quiet — so
any truegoes false when the last reachable lamp disappears, rather than reporting a light thatmay well be dark.
The liveness rule is now
lib/common.js:isThingAlive, shared withget_all_statesso the groupengine and the MCP catalog cannot disagree about what offline means. The heartbeat item is not a
group member but does trigger a recompute — without that a group would keep counting a device for
as long as it stayed silent, which is the exact failure the rule exists to prevent.
Nothing eligible yields
undefinedrather than0orfalse, so a silent group can never read asa real "off" in a Gate. The numeric functions skip non-numbers (a boolean member never counts as 1)
and the boolean functions are strict about
true/false, matchinglib/rules.js.Behaving like an item
The group emits on every member update but moves
laststate/last_changeexactly ashal2Thing.updateStatedoes, sohal2Event's change filters behave identically whether they watcha Thing or a group.
msg.membersays which member moved it. The emit goes straight onto the busrather than through
publishUpdate: a derived value has no business in the history database, andhal2Valuecan only read a group — usehal2Actionto command one.Bugs fixed along the way
hal2Eventwiped its own configuration. Opening a group-configured node resolved the group idwith
RED.nodes.node(); groups are not nodes, sothing,itemandtypeSelwere cleared everytime. That was the whole of "groups don't work in Events".
hal2Gatediscarded group rules for the same reason, one layer down: the rule loader droppedany row whose source did not resolve to a node, so the rule vanished on the next save.
hal2Gateduplicated dynamic rules, pre-existing: the "set bymsg.thing.id" branch calledaddItemtwice, once inside its guard and once after it.hal2Bayeslost saved time-of-day steps —'time'was missing from its source whitelist, soa time step silently reopened as a thing step.
hal2Value's dialog would not open when its source select had no matching option.source passed validation and failed at runtime.
Testing
217 passing, up from 190. The new ones cover every function, the empty and all-offline cases that
must not read as
0/false, vacuousall true/all false, strict booleans, rounding, and thelaststatetransition that decides when "on change" fires.Verified against the running instance: the engine picks the aggregation up at startup, and
medianover nine live members computed 25.68 — matching the members' actual readings — with
last_changecorrectly trailing
last_updatewhile the value held steady.Not yet exercised in production: a member actually going offline, and Gate/Bayes reading a group at
runtime. Only
medianhas run live; the other functions are unit-tested only.