wren node takes switches (plug, socket, appliances), refused when the model lacks the kind
Docs / docs (push) Successful in 24s
Playwright Tests / test-playwright (1, 2) (push) Failing after 32s
Playwright Tests / test-playwright (2, 2) (push) Failing after 21s
pre-commit / pre-commit (push) Failing after 2m56s
Test Backend / test-backend (3.12) (push) Failing after 4m53s
Test Backend / test-backend (3.14) (push) Failing after 4m37s
Compose Smoke Test / test-compose (push) Failing after 24s
Playwright Tests / merge-reports (push) Failing after 2m47s

This commit is contained in:
Melvin Strobl
2026-10-22 17:48:41 +02:00
parent a7a18bf19f
commit 0d4ff89302
4 changed files with 43 additions and 7 deletions
+25 -1
View File
@@ -57,6 +57,10 @@ KIND_WORDS = {
"water": "water",
"watering": "water",
"sprinkler": "water",
"switch": "switch",
"plug": "switch",
"socket": "switch",
"appliances": "switch",
}
#: What the node reads, and the types each may be. Only the prompt wakes it.
@@ -85,6 +89,7 @@ _TOOLS = {
"heat": "heating",
"shutter": "shutters",
"water": "watering",
"switch": "switches",
}
#: What a call sets a target to, by the target's type: (bool, number, text).
@@ -102,6 +107,8 @@ _VALUES: dict[str, tuple[bool | None, float | None, str | None]] = {
"shutters.stop": (None, None, "stop"),
"watering.start": (True, None, "on"),
"watering.stop": (False, None, "off"),
"switches.on": (True, 1, "on"),
"switches.off": (False, 0, "off"),
}
#: An applied call, said: "kitchen light on". `heating.set` says its number.
@@ -117,6 +124,8 @@ _SAID = {
"shutters.stop": "shutters stopped",
"watering.start": "watering on",
"watering.stop": "watering off",
"switches.on": "switch on",
"switches.off": "switch off",
}
#: What every area held in training: a light, and in the living room, which
@@ -160,15 +169,24 @@ _GROUP_KINDS = {
"heating": "heat",
"watering": "water",
"sprinklers": "water",
"switches": "switch",
"plugs": "switch",
"appliances": "switch",
}
#: The call a group command makes on each kind, by its action word.
_GROUP_TOOLS = {
"on": {"light": "lights.on", "media": "media.play", "water": "watering.start"},
"on": {
"light": "lights.on",
"media": "media.play",
"water": "watering.start",
"switch": "switches.on",
},
"off": {
"light": "lights.off",
"media": "media.stop",
"heat": "heating.off",
"water": "watering.stop",
"switch": "switches.off",
},
"open": {"shutter": "shutters.up"},
"up": {"shutter": "shutters.up"},
@@ -194,6 +212,8 @@ _GROUP_SAID = {
"shutters.stop": "shutters stopped",
"watering.start": "watering on",
"watering.stop": "watering off",
"switches.on": "switches on",
"switches.off": "switches off",
}
@@ -426,6 +446,10 @@ class WrenNode(Node):
f"<area>_<kind>, the area one of {', '.join(areas)}, the kind "
f"one of {', '.join(KIND_WORDS)}."
)
if device[1] not in wren.KINDS: # switches came with wren's s10
raise ValueError(
f"'{spec.port}' is a {device[1]}, which this wren does not know."
)
if spec.dtype not in _TARGET_TYPES:
raise ValueError(
f"'{spec.port}' is {spec.dtype.value}; a target is bool, int, "
+1 -1
View File
@@ -34,7 +34,7 @@ class FakeWren:
*("living room", "kitchen", "bathroom", "bedroom", "office"),
*("garden", "patio", "garage", "driveway", "balcony"),
]
KINDS = ["light", "media", "heat", "shutter", "water"]
KINDS = ["light", "media", "heat", "shutter", "water", "switch"]
Device = _Device
House = _House
+9
View File
@@ -40,13 +40,21 @@ def test_targets_of_one_area_and_kind_are_one_device(fake_wren):
("living_room_setpoint", DType.FLOAT),
("living_room_boost", DType.BOOL),
("kitchen_lamp", DType.BOOL),
("office_plug", DType.BOOL),
)
assert node.devices == {
("living room", "heat"): ["living_room_setpoint", "living_room_boost"],
("kitchen", "light"): ["kitchen_lamp"],
("office", "switch"): ["office_plug"],
}
def test_a_kind_the_model_does_not_know_is_refused(fake_wren, monkeypatch):
monkeypatch.setattr(fake_wren, "KINDS", ["light"])
with pytest.raises(ValueError, match="switch"):
_node(("office_plug", DType.BOOL))
@pytest.mark.parametrize("port", ["hallway_light", "kitchen_fridge", "light"])
def test_a_port_naming_no_trained_device_is_refused(fake_wren, port):
with pytest.raises(ValueError, match="living_room") as refused:
@@ -87,6 +95,7 @@ def test_the_model_reads_the_command_plain():
("heating.set", {"temp": 22}, DType.BOOL, None),
("heating.boost", {}, DType.BOOL, True),
("heating.boost", {}, DType.INT, None),
("switches.off", {}, DType.INT, 0),
("sensors.temperature", {"area": "kitchen"}, DType.FLOAT, None),
],
)
+8 -5
View File
@@ -367,9 +367,11 @@ that is not wired to the node.
`garden`, `patio`, `garage`, `driveway`, `balcony`
- **kind**: `light`, `lamp`, `brightness` for a light; `setpoint`, `heating`,
`heat`, `boost` for heating; `media`, `music`, `tv`; `shutter`, `blind`,
`cover`; `water`, `watering`, `sprinkler`
`cover`; `water`, `watering`, `sprinkler`; `switch`, `plug`, `socket`,
`appliances` for a switch, which only wren's s10 model knows
A port naming anything else is refused when the flow is built. A port is
A port naming anything else, or a kind the installed model lacks, is refused
when the flow is built. A port is
named after its message unless it is given a name of its own, in the node
panel's port name field or as `{"name": "setpoint", "port":
"living_room_setpoint"}` through the flow API.
@@ -387,11 +389,12 @@ reads.
| `shutters.up` / `down` | `true` / `false` | `100` / `0` | `up` / `down` |
| `shutters.stop` | — | — | `stop` |
| `watering.start` / `stop` | `true` / `false` | — | `on` / `off` |
| `switches.on` / `off` | `true` / `false` | `1` / `0` | `on` / `off` |
| Setting | Default | Notes |
|---|---|---|
| `apply` | `false` | write to the targets; off, the node only reports |
| `min_confidence` | `0.93` | below this nothing is written |
| `min_confidence` | `0.93` | below this nothing is written. wren's s10 model is less sure of itself, use `0.8` with it |
| `aliases` | empty | the house's own words, or words the recogniser hears wrong, in the model's: `{"traverse": "living room light", "page too": "patio"}` |
Each record in `actions` contains `message`, `value`, `applied`, `delay_s`,
@@ -412,8 +415,8 @@ The model sees every area, wired or not, each with at least a light, so "the
bathroom light" is not taken for a light that is wired elsewhere. Such a call
is reported but not applied, and goes on to `unhandled`. So is a call on an
area the command does not name, unless it is the user's area or, with no area
named, the living room: "switch on the appliances" is a word the model never
learned, and it lands on whichever light it was shown. The command is read
named, the living room: "switch on the appliances" is a word the pinned model
never learned, and it lands on whichever light it was shown. The command is read
in lower case without sentence marks and with number words as digits ("in
five minutes" as "in 5 minutes"), as the model was trained. `aliases` are
applied before, whole words in any case, and `unhandled` carries the prompt