Skip to content

feat(discovery): reusable sweep poll gate, dimmer capability probe, and legacy YAML migration (#733) - #742

Merged
GreenGrassBlueOcean merged 5 commits into
v2-phase1-architecturefrom
arch/issue-733-sweep-gating-and-yaml-migration
Oct 20, 2026
Merged

GreenGrassBlueOcean merged 5 commits into
v2-phase1-architecturefrom
arch/issue-733-sweep-gating-and-yaml-migration

Conversation

@GreenGrassBlueOcean

@GreenGrassBlueOcean GreenGrassBlueOcean commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Refs #733 (parts 1 and 2, and the import half of part 3)

Part 3 of #733 also asks to stop reading myhome.yaml on every start and to simplify PlatformDiscovery. That is not in this PR: while the file exists it stays in charge.

What is the problem

  1. Polls injected into a running sweep. MyHOMEEntity.async_added_to_hass() deferred _poll_on_add only while the one-shot startup sweep ran. An entity discovered during myhome.sweep_bus from a frame without full state (a moving cover, a heating zone) polled at once, mid-sweep (Fresh install without myhome.yaml: lights are never queried, and discovery follow-ups cut status replies short #578).
  2. ON dimmers stay ON/OFF on a fresh install. An ON dimmer can first answer *#1*0## with a plain *1*1*W## (live MH200 capture below), so a bus-discovered light may start as ColorMode.ONOFF with nothing asking it for Dimension 1.
  3. No copy of myhome.yaml. Device settings that only exist in myhome.yaml (travel times, feature flags) are lost when the file is removed or becomes unreadable.

How is it solved

  1. Sweep gate (gateway.py, myhome_device.py)
    • Every send_paced() run, the startup sweep included, is counted in _active_sweeps, exposed as sweep_active. wait_for_initial_discovery() also waits for the sweep to go idle.
    • Entities added while a sweep runs defer _poll_on_add until it is done.
    • close_listener() resets the count and bumps an epoch, so a sweep that was still running before a reconnect cannot end a sweep started after it.
  2. Dimmer capability probe (light.py)
    • A light discovered from the bus during a sweep, ON, without a level, and not feature-locked, sends *#1*W*1## once the sweep is done.
    • The probe is skipped when the sweep itself already promoted the light.
  3. Copy of myhome.yaml in the entry options (legacy_yaml.py, repairs.py)
    • The validated platforms of the gateway's section are stored as entry.options["migrated_yaml_platforms"].
    • The first copy raises the informational legacy_yaml_imported issue. Later starts refresh the copy when the file changed, and an empty section (<mac>: {}) empties it.
    • The copy is used whenever the file yields no section for the gateway: missing, empty, unparsable, invalid, ambiguous top-level platforms with several gateways, or no section for this MAC.
    • A device the validator files under several keys (climate zone aliases, the synthesized button platform) stays one object, both on the live path and when restored from the JSON copy.
    • The options flow keeps the key (it starts from dict(entry.options)). Writing it during setup does not reload the entry (OptionsFlowWithReload, no update listener). The issue is removed with the entry.

How is it tested

tests/test_sweep_pacing.py:

  • Sweep gate: sweep_active during send_paced() and during initial_discovery(); wait_for_initial_discovery() waits for an active sweep; a sweep cut short by close_listener() cannot end the next sweep.
  • Full integration on a mock gateway (F461 model, which polls zones): myhome.sweep_bus is held on its first general request, a moving cover *2*1*19## and zone 2 (*#4*2*0*0215##) are discovered, and neither *#2*19## nor *#4*2… is sent until the sweep is done.
  • Probe: *#1*W*1## after the sweep; no probe for a dimmable light; no probe when a level frame promotes the light during the sweep.
  • YAML copy: import + notice, no rewrite when unchanged, refresh on edit without a second notice, emptying, fallback for each unusable-file case, object identity of aliases.

Full suite against the released OWNd pinned in manifest.json (2.0.0b11): 4353 passed, 100.0% statement coverage. ruff, mypy --strict, verify_ha_standards.py and sync_documentation.py --check are clean.

Where are the traces

What does the capture / Oracle / Encyclopedia say

Request Device Bus answer
*#1*81*1##, *#1*84*1##, *#1*57*1## relay, ON *1*1*W##
*#1*11*1## relay, OFF *1*0*11##
*#1*29*1## dimmer, ON *1*1*29##, *#1*29*1*200*2##, *1*1*29##
*#1*67*1## dimmer, ON *1*1*67##, *#1*67*1*174*2##, *1*1*67##
  • Relays: a relay answers a Dimension 1 request with its plain status and no level. It neither NACKs on the bus nor stays silent, so the probe cannot promote a relay. The command-session ACK/NACK was not recorded.
  • Dimmers in the sweep: the ON dimmers 29 and 67 first answered *#1*0## with *1*1*W## and only later in the same sweep with their level (*1*10*29##, *1*9*67##). Most ON dimmers are therefore promoted by the sweep itself, and the probe only covers the ones that never send their level.
  • Firmware oracle: *#1*31*1## (no device behind it) is put on the bus by every emulated gateway except the F450, which drops it. The command session NACKs it, except on the MH202. The oracle has no data on how probes and sweeps interleave.
  • Encyclopedia KB: it has no record of how an on/off actuator answers DIMENSION 1. It records that an F454 answered a Dimension 1 request on an OFF F418U2 as DIMENSION 4. The probe only targets ON lights, but the answer of an F454 to an ON relay is unrecorded.

🤖 Generated with Claude Code

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-734-bus-truthful-state-and-resync branch from f941dc1 to 1aedd67 Compare October 17, 2026 10:15
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-733-sweep-gating-and-yaml-migration branch from f77599f to 91ce456 Compare October 17, 2026 10:16
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-734-bus-truthful-state-and-resync branch from 1aedd67 to 7eb1348 Compare October 17, 2026 10:40
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-733-sweep-gating-and-yaml-migration branch from 91ce456 to 461550b Compare October 17, 2026 10:40
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-734-bus-truthful-state-and-resync branch from 7eb1348 to 348d40b Compare October 19, 2026 09:49
GreenGrassBlueOcean added a commit that referenced this pull request Oct 19, 2026
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-733-sweep-gating-and-yaml-migration branch from 461550b to 0479504 Compare October 19, 2026 09:54
GreenGrassBlueOcean added a commit that referenced this pull request Oct 19, 2026
… packages (#732)

- Rebase onto #742 (0479504) and latest v2-phase1-architecture (fcdb507)
- Add test_replay_f461_who14_live_2026_10_18_gateway_identity replaying live BTicino F461 (2.0.11, kernel 5.10.35, distribution 2.26.4) WHO=13 Dims 15/16/23/24 and WHO=1013 Dim 1
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-733-sweep-gating-and-yaml-migration branch from 0479504 to 9a83cf6 Compare October 19, 2026 12:37
@GreenGrassBlueOcean
GreenGrassBlueOcean force-pushed the arch/issue-734-bus-truthful-state-and-resync branch from 348d40b to e5ea6cd Compare October 19, 2026 12:37
Base automatically changed from arch/issue-734-bus-truthful-state-and-resync to v2-phase1-architecture October 20, 2026 19:06
GreenGrassBlueOcean and others added 2 commits October 20, 2026 21:22
…nto arch/issue-733-sweep-gating-and-yaml-migration
…ml copy current (#733, #742)

Sweep gate
- Count the startup sweep like any other paced sweep; drop the private
  _startup keyword.
- close_listener() bumps a sweep epoch, so a sweep still running before a
  reconnect cannot end a sweep started after it.

myhome.yaml copy in the entry options
- Refresh the copy whenever the validated section changed; only the first
  copy raises legacy_yaml_imported. An empty section (<mac>: {}) empties it.
- Fall back to the copy whenever the file yields no section for the
  gateway: missing, empty, unparsable, invalid, ambiguous top-level
  platforms, or no section for this MAC.
- Keep a device that the validator files under several keys (zone aliases,
  the button platform) one object, live and when restored from JSON.
- Store the copy as plain JSON; drop the test-only guards.

Repair text, docs and the upgrade guide describe the refresh and fallback;
fr/it/nl now carry the full description.

Tests: reconnect race, startup sweep counted, full-integration
myhome.sweep_bus run holding back a new cover and heating zone, real
Dimension 1 promotion during a sweep, YAML refresh/fallback/identity. The
blank-start old-path test now waits for the startup sweep instead of
faking its end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit 0d90598 into v2-phase1-architecture Oct 20, 2026
16 checks passed
@GreenGrassBlueOcean
GreenGrassBlueOcean deleted the arch/issue-733-sweep-gating-and-yaml-migration branch October 20, 2026 20:01
GreenGrassBlueOcean added a commit that referenced this pull request Oct 20, 2026
… packages (#732)

- Rebase onto #742 (0479504) and latest v2-phase1-architecture (fcdb507)
- Add test_replay_f461_who14_live_2026_10_18_gateway_identity replaying live BTicino F461 (2.0.11, kernel 5.10.35, distribution 2.26.4) WHO=13 Dims 15/16/23/24 and WHO=1013 Dim 1
GreenGrassBlueOcean added a commit that referenced this pull request Oct 20, 2026
)

Bring in #742 (sweep gating, myhome.yaml copy in entry options) and
#744 (#732 modularization of config_flow, gateway and platforms).

Conflicts:
- validate.py: import list; keep both CONF_STEP_*_ACTION and
  CONF_STRATEGY.
- legacy_yaml.py: keep the base's _load_live_yaml/_apply_yaml_platforms
  structure and re-apply the expose handling on top: a top-level
  `expose:` key counts as single-gateway config, the where-filling loop
  skips `expose`, and the validated expose list is collected into
  configured_expose (threaded through _load_live_yaml).
- config_flow.py: take the base's split module and move
  ExposeSubentryFlowHandler into config_flow_expose.py, next to
  config_flow_options.py; config_flow.py keeps
  async_get_supported_subentry_types and re-exports the handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants