Skip to content

fix(ha): admit fan entities to the graph so fans are controllable#638

Open
minion1227 wants to merge 1 commit into
GeniePod:mainfrom
minion1227:minion_fan_domain
Open

fix(ha): admit fan entities to the graph so fans are controllable#638
minion1227 wants to merge 1 commit into
GeniePod:mainfrom
minion1227:minion_fan_domain

Conversation

@minion1227

Copy link
Copy Markdown
Contributor

Summary

Fans are silently uncontrollable: fan is a first-class control domain everywhere in the stack except the one gate that admits entities into the resolution graph, so every fan.* entity is dropped before resolution and any fan command fails.

The bug

fan is treated as controllable throughout:

  • the quick router routes "turn on the ceiling fan in the bedroom" to home_control (shipped test, quick.rs), and lists fan as a known device (#523);
  • infer_domain iterates ["light","switch","fan","climate","cover","lock"] and domain_of_word maps "fan"/"fans" → "fan";
  • execute() drives TurnOn/TurnOff/Toggle generically via fan.turn_on etc.

But is_voice_relevant_domain — the filter build_graph uses to decide which entities enter the graph — omitted fan. So:

HA has fan.bedroom_ceiling in area Bedroom. User: "turn on the bedroom fan." The router emits home_control{entity:"bedroom fan", action:"turn_on"}; resolution filters graph.entities by domain == "fan" — but the fan was never admitted — and falls through to no Home Assistant target matched 'bedroom fan'. The device exists and would actuate fine.

This is an oversight, not a policy: fans aren't sensitive (not in the confirmation list), nothing marks fan as excluded, and four other code paths already treat it as controllable.

Changes

Add fan as a first-class control domain in ha/provider.rs, mirroring switch:

  • is_voice_relevant_domain admits fan — the actual fix that lets fan entities into the graph.
  • domain_synonyms["fan","fans"]; domain_label / domain_plural_label → fan singular/plural, so status summaries say "fan"/"fans" instead of the generic "devices".
  • capabilities_for reports turn_on/turn_off for fans.

Real Behavior Proof

  • I have built and run the affected code locally (or noted why I could not).
  • I have NOT verified on Jetson hardware, and I explain the equivalent verification path or validation gap below.

Tested profile / hardware (check all that apply):

  • laptop

No Jetson-specific path; the resolution logic is deterministic and unit-testable against an in-memory graph, which is the equivalent verification.

What I ran

Linux laptop, x86-64, on this branch (based on current main):

cargo fmt --all -- --check
cargo clippy -p genie-core --all-targets --locked -- -D warnings
cargo test -p genie-core --locked
# regression proof: revert only the is_voice_relevant_domain change and re-run the new test

What I observed

  • fmt clean; clippy clean under -D warnings; all 991 genie-core tests pass, 0 failures.
  • New test build_graph_admits_fan_entities_and_resolves_them: builds a graph from one fan.bedroom_ceiling entity, asserts it is admitted to graph.entities, and that resolve_target_in_graph("bedroom fan", TurnOn) resolves to it with domain == "fan".
  • Regression proof: with only the is_voice_relevant_domain fan line reverted, the new test FAILS (panicked: fan entity should be admitted to the graph); restored, it passes. So the test genuinely captures the bug.

Notes for reviewers

Scoped to provider.rs, additive, and modeled exactly on the existing switch domain. execute() already handles the generic on/off/toggle service call, so no actuation changes are needed — the fix is purely admitting fans to the graph and giving them proper labels.

`fan` is treated as a controllable domain throughout the stack — the
quick router emits `home_control` for "turn on the ceiling fan" (shipped
test), `infer_domain` and `domain_of_word` both resolve the `fan`
domain, and `execute()` drives `fan.turn_on`/`turn_off`/`toggle`
generically. But `is_voice_relevant_domain` — the gate `build_graph`
uses to admit entities — omitted `fan`, so every `fan.*` entity was
dropped from the graph before resolution. The result: "turn on the
bedroom fan" failed with "no Home Assistant target matched", even though
the fan existed and would actuate fine.

Add `fan` as a first-class control domain, mirroring `switch`:
- `is_voice_relevant_domain` admits `fan` (the actual fix)
- `domain_synonyms`, `domain_label`, `domain_plural_label` give fans
  proper spoken/display strings instead of the generic "devices"
- `capabilities_for` reports turn_on/turn_off for fans

Regression test builds a graph from a `fan.*` entity and asserts it is
admitted and resolves; it fails without the gate change.
@github-actions github-actions Bot added the bug Something isn't working label Jul 5, 2026

@matedev01 matedev01 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — admits fan entities to the HA graph. Clean HA fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants