Add commands on inputs that push CMD_SWITCH or a mode to another node #146

Merged
Claude merged 5 commits from feat/yn-30-commands into feat/yaml-nodes 2026-10-18 06:21:25 +00:00
Collaborator

Binds pull; inputs now push. A switch, touch or occupancy takes commands: an endpoint command (node, endpoint, action of toggle, on, off or follow) sends CMD_SWITCH to another node's output, and - mode: <name> broadcasts a mode. This changes a merged format: the YN-18 action: mode bind moves into commands, inputs no longer take binds, and the sourceless-bind rule is gone. The YN-18 tests are rewritten for the new shape.

Runtime (shared paths, for review): ModeStrategy becomes CommandStrategy and handles three sourceless actions, Mode, Command and FollowCommand. Bind gains targetNode, targetEndpoint and commandAction. A fresh press sends the command (a mode also applies locally, as before); follow sends On or Off as the input changes. disabled_modes and the input's enabled_by gate all of them. Nothing new is received, so the interests are unchanged. A node does not hear its own bus frames, so the generator rejects a command whose target is on the same node and says to put a bind on that output instead (the ticket allowed it; review asked for the rejection).

Fixed nodes: a node file with fixed: true describes a hand-written node by label, with id as the wire ID, no board and endpoints of index, type and label only. The ticket said 1 to 10, but the hand-written IDs in node_ids.h run up to 0xC0 (Lighting is 0x10), so the range is 1 to 254 and check_unique compares wire IDs (a fixed ID 16 collides with a built node whose ID is 6). The generator never builds a fixed node, and the build script skips it. A number in node: always means a YAML ID, never a fixed node's. Fixed nodes can also be bind sources.

Generator: commands resolve by label or number as binds do, and the target must be a light, fan, load, dimmable_light, variable_fan, colour_light, water_heating or flow5d. YAML reads an unquoted on and off as booleans, so both spellings are accepted (the schema allows true and false). Schema goes through the Defs registry with a fixed form selected by an if/then/else at the root; README gains Commands and Fixed nodes sections; the C3 fixture uses commands and there is a fixed-node fixture (fixed_lighting.yaml).

Tested: native 651 -> 657, Python 163 (with jsonschema); pio run and scripts/build_yaml_nodes.sh pass.

YN-30

Binds pull; inputs now push. A `switch`, `touch` or `occupancy` takes `commands`: an endpoint command (`node`, `endpoint`, `action` of `toggle`, `on`, `off` or `follow`) sends `CMD_SWITCH` to another node's output, and `- mode: <name>` broadcasts a mode. This changes a merged format: the YN-18 `action: mode` bind moves into `commands`, inputs no longer take `binds`, and the sourceless-bind rule is gone. The YN-18 tests are rewritten for the new shape. Runtime (shared paths, for review): `ModeStrategy` becomes `CommandStrategy` and handles three sourceless actions, `Mode`, `Command` and `FollowCommand`. `Bind` gains `targetNode`, `targetEndpoint` and `commandAction`. A fresh press sends the command (a mode also applies locally, as before); `follow` sends On or Off as the input changes. `disabled_modes` and the input's `enabled_by` gate all of them. Nothing new is received, so the interests are unchanged. A node does not hear its own bus frames, so the generator rejects a command whose target is on the same node and says to put a bind on that output instead (the ticket allowed it; review asked for the rejection). Fixed nodes: a node file with `fixed: true` describes a hand-written node by label, with `id` as the wire ID, no board and endpoints of `index`, `type` and `label` only. The ticket said 1 to 10, but the hand-written IDs in `node_ids.h` run up to 0xC0 (Lighting is 0x10), so the range is 1 to 254 and `check_unique` compares wire IDs (a fixed ID 16 collides with a built node whose ID is 6). The generator never builds a fixed node, and the build script skips it. A number in `node:` always means a YAML ID, never a fixed node's. Fixed nodes can also be bind sources. Generator: commands resolve by label or number as binds do, and the target must be a `light`, `fan`, `load`, `dimmable_light`, `variable_fan`, `colour_light`, `water_heating` or `flow5d`. YAML reads an unquoted `on` and `off` as booleans, so both spellings are accepted (the schema allows true and false). Schema goes through the `Defs` registry with a `fixed` form selected by an if/then/else at the root; README gains Commands and Fixed nodes sections; the C3 fixture uses commands and there is a fixed-node fixture (`fixed_lighting.yaml`). Tested: native 651 -> 657, Python 163 (with jsonschema); `pio run` and `scripts/build_yaml_nodes.sh` pass. YN-30
Add commands on inputs and fixed node files, replacing the mode bind
Some checks failed
Build and test / build (pull_request) Has been cancelled
Build and test / release (pull_request) Has been cancelled
01012ba04f
Claude left a comment

Runtime change is sound: null targets are safe on every shared path, Mode behaves as before, and follow still sends Off in a disabled mode. Points to act on below. I could not read YN-30, so the first is checked against node_ids.h only.

Runtime change is sound: null targets are safe on every shared path, Mode behaves as before, and follow still sends Off in a disabled mode. Points to act on below. I could not read YN-30, so the first is checked against node_ids.h only.
@ -410,0 +417,4 @@
raise fail(path, field, "needs a mode, or an endpoint and an action")
else:
if isinstance(raw["action"], bool):
raw = {**raw, "action": "on" if raw["action"] else "off"}
Author
Collaborator

The bool-to-on/off conversion has no test. Delete these two lines and no test fails, since neither the parse tests nor the schema tests pass True or False as an action. Add a case to each.

The bool-to-on/off conversion has no test. Delete these two lines and no test fails, since neither the parse tests nor the schema tests pass True or False as an action. Add a case to each.
@ -556,1 +579,4 @@
def parse_fixed_node(header, raw_endpoints, path):
reject_unknown(path, "node", header, {"id", "label", "fixed"})
check_label(path, "node.label", header.get("label"))
Author
Collaborator

Fixed node id is limited to 1..10, but the hand-written nodes in node_ids.h are 0x01, 0x02, 0x10-0x14, 0x40, 0x41, 0x60, 0xA0, 0xA1, 0xC0. Lighting (0x10) cannot be declared, and the README example labels id 1 'Lighting' when 1 is the Bridge. Unless YN-30 says 1..10 on purpose, widen to 1..maxNodeId (schema and README too). If the range stays, check_unique rejecting fixed id 1 against YAML id 1 is spurious: their wire ids are 1 and 11.

Fixed node id is limited to 1..10, but the hand-written nodes in node_ids.h are 0x01, 0x02, 0x10-0x14, 0x40, 0x41, 0x60, 0xA0, 0xA1, 0xC0. Lighting (0x10) cannot be declared, and the README example labels id 1 'Lighting' when 1 is the Bridge. Unless YN-30 says 1..10 on purpose, widen to 1..maxNodeId (schema and README too). If the range stays, check_unique rejecting fixed id 1 against YAML id 1 is spurious: their wire ids are 1 and 11.
@ -727,0 +803,4 @@
if endpoint is None:
raise fail(path, f"{field}.endpoint", f"{reference!r} is not an endpoint of {target['label']!r}")
if endpoint["type"] not in commandTargets:
raise fail(path, f"{field}.endpoint", f"{reference!r} is a {kind_name(endpoint)}, which cannot take a switch command")
Author
Collaborator

A command whose target is this node is generated, and the README says it does nothing because a node does not hear its own frames. Reject it here and say 'use a bind' instead of emitting a silent no-op. test_commands_become_sourceless_binds_from_their_own_input (bind4, Porch light) pins the no-op as valid.

A command whose target is this node is generated, and the README says it does nothing because a node does not hear its own frames. Reject it here and say 'use a bind' instead of emitting a silent no-op. test_commands_become_sourceless_binds_from_their_own_input (bind4, Porch light) pins the no-op as valid.
@ -858,0 +922,4 @@
CommandRig gated;
gated.bind.enabled = gate;
gateOpen = false;
Author
Collaborator

No test that a follow command still sends Off in a disabled mode. That is the one deliberate asymmetry in CommandStrategy::onSource, and removing it would pass every current test. The disabled-mode test also has no positive control, so it would pass if setMode(Away) did nothing.

No test that a follow command still sends Off in a disabled mode. That is the one deliberate asymmetry in CommandStrategy::onSource, and removing it would pass every current test. The disabled-mode test also has no positive control, so it would pass if setMode(Away) did nothing.
Allow any fixed node ID, warn on own-node commands and pin more tests
All checks were successful
Build and test / build (pull_request) Successful in 2m13s
Build and test / release (pull_request) Has been skipped
2e9aea2a6c
rob requested changes 2026-10-18 06:15:46 +00:00
@ -168,2 +184,3 @@
const BindStrategy* strategyFor(const Bind& bind) {
if (bind.target == nullptr) return bind.action == BindAction::Mode ? &modeStrategy : nullptr;
if (bind.target == nullptr)
return bind.action == BindAction::Mode || bind.action == BindAction::Command || bind.action == BindAction::FollowCommand ? &commandStrategy : nullptr;
Owner

Refactor this logic chain to an isCommand method

Refactor this logic chain to an `isCommand` method
Reject a command that targets its own node
Some checks failed
Build and test / build (pull_request) Has been cancelled
Build and test / release (pull_request) Has been cancelled
c08aaef02a
Name the sourceless command check on Bind
All checks were successful
Build and test / build (pull_request) Successful in 2m11s
Build and test / release (pull_request) Has been skipped
16214ec5d3
Claude merged commit bbb0f0c388 into feat/yaml-nodes 2026-10-18 06:21:25 +00:00
Sign in to join this conversation.
No reviewers
rob
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
CamperCan/CampervanControlSystems!146
No description provided.