Skip to content

feat(config_flow): identify a manually added gateway after authentication - #804

Merged
GreenGrassBlueOcean merged 8 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:feat/config-flow-identify-manual-gateway
Oct 25, 2026
Merged

GreenGrassBlueOcean merged 8 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:feat/config-flow-identify-manual-gateway

Conversation

@gdluck

@gdluck gdluck commented Oct 24, 2026 •

Copy link
Copy Markdown

Requested by A

What is the problem

A gateway added by hand keeps whatever MAC and model the user typed. The form defaults (00:03:50:00:00:00, MyHomeServer1) are easy to leave in place, and docs/configuration/gateway-identification.md already says a manual label is "trusted, but correctable by certain evidence" and that early manual flows defaulting to F454 produced mislabelled entries. The runtime identity resolver (#744) corrects the model later, but the unique_id (the MAC) and the entry title are fixed at creation, so a placeholder MAC stays the unique_id forever and a second gateway with the same placeholder cannot be added.

Separately, password_required from the test-connection step was logged at WARNING although it is the normal first answer of every password-protected gateway.

Refs #420 (identification order), #744 (runtime identity resolver), #750 ("unverified means unsupported").


What is the solution

  1. config_flow_discovery.py:
    • identify_gateway(gateway) opens one command session on the authenticated gateway and returns a GatewayIdentity(mac, who13_code, who1013_code, password_required), best effort, 15 s cap. The order follows the runtime identification of Fix false gateway identity mismatch (#370) by using WHO=1013 diagnostics #420: *#13**15## first (answered on all 12 oracle gateways); *#1013**1## only when the WHO 13 answer is a shared code (read_who13(code).shared, i.e. WHO13_SHARED_DEVICE_TYPES: 200, the F454 / F461 / MyHomeServer1 class); *#13**12## (MAC) only when every model the reported codes can name is in MAC_REQUEST_SAFE_MODELS, which holds only the models a physical unit answered with its real MAC: F461 2.0.11 and MyHomeServer1 3.87.13. An unknown or unlisted model is never asked its MAC (fail closed); a model that merely did not crash in emulation does not qualify. The probe logs through PROBE_LOGGER, a registered child logger whose filter lowers every OWNd line to DEBUG: its refusals are expected.
    • _async_apply_gateway_identity, run only for the manual path after the gateway accepted the password:
      • a reported MAC is accepted only if it passes the form's MACAddress validation (six octets, 0..255) and is neither 00:00:00:00:00:00 (the factory value, also the "Custom (IP Gateway)" sentinel of the first form) nor ff:ff:ff:ff:ff:ff; a usable MAC that differs from the typed one replaces it, anything else leaves the typed MAC in place (DEBUG line);
      • the model goes through identity.resolve_gateway_identity() with GatewayIdentityEvidence(manual=<typed model, or None for Generic / Gateway / Unknown>, who13_code, who1013_code), the same verdict the runtime reaches: a brand variant of the reported model is kept (H4890 / LN4890 on OBJECT_MODEL 30, 003598 on 51), an uncertain Nmap WHO 13 code may label a Generic entry but never overrides a chosen model, a certain code corrects it; the OWNd profile and log_id follow, and resolution.source is persisted as model_source so diagnostics report who13 for a corrected entry;
      • a command session refused with password_required (open monitor session *99*1##, protected command session *99*0##) sends the flow to the password step once, since the runtime command workers need that password anyway; the probe then runs again with it. A second refusal (e.g. after a blank password) is treated as no answer, so the flow cannot loop.
    • async_step_test_connection sets the unique_id on the manual path only after identification, from the final dr.format_mac(gateway.serial), then refreshes the flow context and title placeholders, then persists model_source. A true duplicate still aborts already_configured; a second gateway typed with the placeholder MAC gets to report its real one first.
    • password_required from the test connection logged at DEBUG; other failures stay WARNING.
  2. config_flow.py: the manual step no longer sets the unique_id before authentication; it sets _identify_after_auth and leaves the unique_id to the connection test. identify_gateway and GatewayIdentity are re-exported for patching.
  3. identity.py: UNTRUSTED_MODEL_LABELS ("", generic, gateway, unknown) names the labels that are no model, shared by identification_source and the flow.
  4. docs/configuration/gateway-identification.md: a "Setup time" section documents the probe order, the MAC_REQUEST_SAFE_MODELS crash gate, the MAC validation, the resolver verdicts and the split-authentication case; the *#1013*0*1## example reads *#1013**1##.
  5. tests/test_config_flow_identify.py (new, 29 cases) and the three patched manual-flow tests in tests/test_config_flow.py. Fixtures use 192.0.2.92 and 00:03:50:00:07:77.
  6. .github/release-notes/2.0.0b16.md: one line under "Other changes".

MAC_REQUEST_SAFE_MODELS is a MyHOME-side table on purpose for now: whether a gateway survives and answers *#13**12## is a firmware fact that belongs in an OWNd profile flag (mac_request_safe, tri-state) once the oracle rows are published (#381 pattern). Until then the gate fails closed and is widened only by a live capture of a real MAC.

Companion PR: none in OWNd. The dependency is evidence, not code: oracle PR OpenWebNet-HA/own-firmware-oracle#59 publishes the crash rows this gate rests on.

Review follow-up (review 5639869140)

  1. Model resolution delegated to resolve_gateway_identity(); brand variants kept, uncertain codes never override a manual choice, model_source persisted, profile and log_id refreshed (tests: H4890 / LN4890 kept on 200 → 30, 003598 kept on 51, code 44 labels Generic → MH200N but leaves MH202, Generic labelled F461 from 134, model_source who13 / manual).
  2. MAC validation before the reply replaces the typed value: *#13**12*0*0*0*0*0*0##, *#13**12*255*255*255*255*255*255## and *#13**12*256*3*80*0*7*119## (OWNd formats the octet as 100) all leave the user's MAC and unique_id intact.
  3. unique_id deferred on the manual path until after identification: an existing manual F454 holding 00:03:50:00:00:00 no longer blocks an F461 typed with the same placeholder (it learns 00:03:50:00:07:77 and the entry is created); two gateways whose MAC is never learned still cannot share the placeholder (already_configured after authentication). test_form_already_configured now patches identify_gateway and asserts the abort happens after the probe.
  4. Split authentication routed to the password step once, probe re-run with the password, OWNd's password_required lines at DEBUG through PROBE_LOGGER; a blank password does not bring the form back.
  5. Polish: flow context and title_placeholders refreshed after identification (pinned through the bus topology step), WHO13_SHARED_CODE replaced by read_who13(code).shared, worker-count test (MyHomeServer1 corrected to MH200 via *#13**15*4## or H4684 via *#13**15*13## starts one command worker).
  6. PR description: the feat(oracle): add who13-identification suite (network dimensions crash H4684 and MH200) own-firmware-oracle#59 table interpretation corrected below; V2 checklist item ticked.
  7. Docs: the setup-time probe and the MAC_REQUEST_SAFE_MODELS gate documented; *#1013**1## example aligned.

An independent second-opinion review of the follow-up diff added the ask-once guard (a blank password is accepted by the form and would have re-asked forever) and the registered child logger (logging.getLogger(PROBE_LOGGER.name) must be the same object for caplog.set_level(..., logger=...) to work).


State Diagram

stateDiagram-v2
    direction TB
    state "Manual config flow once the monitor session is accepted" as Flow {
        [*] --> Identify : custom_manual form submitted , host, typed MAC, selected model - no unique_id yet
        Identify --> Password : command session answers password_required and no password given - ask once, then identify again
        Identify --> Who13 : command session open - *#13**15## , 15 s cap on the whole step
        Who13 --> Who1013 : shared code 200 - *#1013**1##
        Who13 --> MacGate : code 13 / 4 / 0 / none - model from WHO 13 only
        Who1013 --> MacGate : object model known , 134 F461, 67 MyHomeServer1, 30 AM4890 family, ... or none
        MacGate --> Mac : every model the codes can name is in MAC_REQUEST_SAFE_MODELS {F461, MyHomeServer1} - *#13**12##
        MacGate --> Resolve : otherwise , H4684, MH200, F454, unknown - the MAC is never asked
        Mac --> Resolve : reply validated - six octets 0..255, not all-zero, not broadcast - else typed MAC kept
        Resolve --> UniqueId : resolve_gateway_identity , manual choice kept on a brand variant or an uncertain code, corrected by a certain code, Generic labelled - model_source recorded
        UniqueId --> AlreadyConfigured : final MAC has an entry - abort already_configured
        UniqueId --> Entry : context and title refreshed - entry created
        Identify --> UniqueId : session refused, no Success, timeout, or any exception - typed values kept
    }
    state "Before this PR" as Before {
        [*] --> TypedValues : entry created with the typed MAC and model, no question asked
    }
Loading

Hardware Verification

  • Physical F461 2.0.11, local Home Assistant 2026.10.2 (test instance): manual flow with host, MAC 00:03:50:00:00:00 and model MyHomeServer1.
  • Live retest 2026-10-24 (report-15 consensus label confirmed for the manual flow / re-add): entry deleted and re-added through the REST flow (user → custom → custom_manual with the placeholder MAC and model → password). The probe on the event session saw *#13**15*200##, *#1013**1*134*15*5*0## (twice) and the *#13**12*…## MAC reply (the real MAC, rewritten to the fixture's synthetic *#13**12*0*3*80*0*16*35## / 00:03:50:00:10:23 in this text); HA logged Gateway at <host> reports MAC 00:03:50:00:10:23; using it instead of the entered 00:03:50:00:00:00 and … reports WHO=1013 OBJECT_MODEL 134; using it instead of the selected model MyHomeServer1; create_entry titled F461 Gateway, entry data name: F461, diagnostics identification.model: F461, profile F461Profile; no WARNING for password_required. Adding it again aborts already_configured. The entry then imported the plant yaml and started normally. The runtime does not send *#13**15## / *#1013**1## at startup for a stored manual entry (identification.who13_code: null; only myhome.sweep_bus and this flow ask), so startup enrichment of a stored entry does not occur: runtime design, not a regression. The review follow-up changes the verdict path (resolver, MAC validation, unique_id timing) but not the frames sent, and the unit tests replay the same replies; the live run was not repeated.
  • Physical MyHomeServer1 3.87.13 (read-only probe 2026-10-24, no command sent): *#13**15## → *#13**15*200##; *#1013**1## → *#1013**1*67*15*5*0## (model 67 = MyHomeServer1); *#13**27## → the ASCII bytes of "MyHomeServer1"; *#13**12## → its real MAC (synthetic in the fixture). The emulated MyHomeServer1 2.82.06 factory image answers *#13**12*0*0*0*0## (feat(oracle): add who13-identification suite (network dimensions crash H4684 and MH200) own-firmware-oracle#59 prose), so the real-MAC answer is a 3.87.13 fact; the flow handles both (a four-value reply decodes to no MAC in OWNd, a six-zero reply is refused by the validation, and the typed MAC stays either way).

How is it tested

  • tests/test_config_flow_identify.py (29 cases, 21 functions) and the three patched manual-flow tests in tests/test_config_flow.py (test_form_already_configured, test_password_required_and_error, test_custom_manual_entry_manufacturer_type).
  • Fail-before / pass-after per commit, on the tree before each one: resolver commit 8 fail (6 new cases, 2 existing ones gaining model_source asserts); MAC validation 4 fail; deferred unique_id 3 fail (2 new, test_form_already_configured); split authentication 8 fail (3 new, 5 existing cases updated to GatewayIdentity); polish 3 fail; second-opinion follow-up 2 fail. All pass after.
  • Full suite against OWNd master (f04d8aa, the behaviour of the 2.0.0b11 pin), rebased on v2-phase1-architecture 798d82c: 4552 passed, 1 skipped, 100 % statement coverage (16,190 / 16,190).
  • scripts/verify_ownd_coverage.py 100 %, scripts/typing_ratchet.py 0 errors (baseline 0), ruff check ., scripts/verify_ha_standards.py, scripts/sync_documentation.py --check, scripts/manage_translations.py check all pass.
  • CI note: an earlier push's HA Compatibility (beta) job failed on tests/test_door_entry_events.py::test_doorbell_fires_once_on_a_shared_bus, a timing-sensitive standby-failover test this PR does not touch (same job green on the sibling PRs from the same base); re-run once per the maintainers' flaky-test practice.

Live retest (2026-10-24)

Consensus label: confirmed for the manual flow / re-add (frames and log lines above); startup enrichment of a stored manual entry does not occur by runtime design.


Where are the traces

Subsystem Capture Plant Frames verified by this PR
WHO 13 / 1013 identification exchange tests/fixtures/traces/f461_live_2026-10-23/ run run1_oracle_replay_readonly, steps oracle_who13 and plant_reads (trace PR #799, merged) F461 2.0.11 *#13**15## → *#13**15*200## (11 ms); *#13**12## → *#13**12*0*3*80*0*16*35## (10 ms, synthetic bytes of the fixture MAC, note: who13_mac_answered_on_f461); the spontaneous *#1013**1*134*15*5*0## that opens every event session
Same, second gateway family tests/fixtures/traces/myhomeserver1_live_2026-10-24/ run run1_identify (trace PR #799, merged) MyHomeServer1 3.87.13 *#13**15## → *#13**15*200## (43 ms); *#1013**1## → *#1013**1*67*15*5*0## (38 ms); *#13**12## → *#13**12*0*3*80*0*32*36## (38 ms, synthetic bytes, note: who13_mac_answered_real_value_scrubbed); *#13**27## → the model name as ASCII codes
*#13**12## crash on H4684 / MH200 No physical trace: real gateway daemons under QEMU only (oracle PR OpenWebNet-HA/own-firmware-oracle#59) emulated H4684 020054, MH200 020102 *#13**12## → nack, verdict crash

What does the Firmware Oracle / Encyclopedia say

  1. Published (index 53f9514), results/<P>/<V>/oracle/full/trace-corpus.tsv, *#13**15##: ack / out on all 12 emulated gateways; 200 on F450, F454, F459, F460, F461, MH202, MyHomeServer1; 0 on F453, F453AV, MH200N; 13 on H4684; 4 on MH200. The basis for asking it first and for treating 200 as shared.
  2. Oracle suite who13-identification (feat(oracle): add who13-identification suite (network dimensions crash H4684 and MH200) own-firmware-oracle#59; harness fix fix(sandbox): pin the dummy eth0 MAC so WHO 13 DIMENSION 12 is reproducible own-firmware-oracle#57, issue Reproducing all 288 committed suites locally: one date-dependent clock row, eight stale sound-who22 TSVs, and a random dummy MAC own-firmware-oracle#58), *#13**12##. Its findings/who13-identification.md table carries only five of the twelve columns with values (F450, F454, H4684, MH200, MH202); the — cells of the other seven (F453, F453AV, F459, F460, F461, MH200N, MyHomeServer1) are elided columns of the local markdown, not a verdict: those gateways answer *#13**15##, *#13**16## and *#1013**1## with out frames in the published trace-corpus.tsv. The real split is in the fix(sandbox): pin the dummy eth0 MAC so WHO 13 DIMENSION 12 is reproducible own-firmware-oracle#57 / feat(oracle): add who13-identification suite (network dimensions crash H4684 and MH200) own-firmware-oracle#59 prose: H4684 020054 and MH200 020102 crash (nack, crash; reproduced in three fresh runs on each); F450 020010, F454 020051 and MH202 010024 answer ack / out with the jail interface's bytes (e.g. F454 *#13**12*26*137*158*147*25*187##; the values are the harness host's and vary with it, the verdict does not); F453, F453AV and MH200N ACK with no bus output; F459, F460, F461 and MyHomeServer1 answer *#13**12*0*0*0*0## from the factory configuration (four values: OWNd decodes no MAC from it). *#13**15## / *#13**16## / *#1013**1## are answered normally before and after the crash.
  3. Live, from the merged test(trace): ingest live F461 2.0.11 2026-10-23 capture and MyHomeServer1 3.87.13 reads behind OWNd #136-#142 and MyHOME #791-#798 #799 fixtures: the physical F461 2.0.11 answers *#13**12## with its real MAC (*#13**12*0*3*80*0*16*35## after scrubbing, note: who13_mac_answered_on_f461, 10 ms) and the physical MyHomeServer1 3.87.13 with its real MAC as well (*#13**12*0*3*80*0*32*36## after scrubbing, note: who13_mac_answered_real_value_scrubbed, 38 ms). The emulated no-output / four-zero rows are therefore a 2.0.11-image / 2.82.06-image fact, not the physical behaviour, which is why the whitelist holds live-confirmed models only and why a six-zero or broadcast reply is refused rather than trusted.

Checklist

🤖 Generated with Claude Code

@gdluck
gdluck force-pushed the feat/config-flow-identify-manual-gateway branch from ef7be41 to af65f3b Compare October 24, 2026 13:27
@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!

gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 24, 2026
MyHomeServer1 README: "OWnd" typo; the alarm gate, the config-flow
identification and the liveness probe rows name MyHOME OpenWebNet-HA#803, OpenWebNet-HA#804, OpenWebNet-HA#806.
F461 README: the "What this capture backs" table extends to MyHOME OpenWebNet-HA#800-OpenWebNet-HA#806
(OpenWebNet-HA#802 the dimension 27 wire form, OpenWebNet-HA#803 *michnovka#5*0##, OpenWebNet-HA#804 the WHO 13 / 1013
identification replies, OpenWebNet-HA#806 the *OpenWebNet-HA#13**0## echo; OpenWebNet-HA#800 / OpenWebNet-HA#801 / OpenWebNet-HA#805 have no
frame in this capture); it states that f461_live_2026-10-15 holds the
dimension 22 reboot behind OWNd OpenWebNet-HA#136, the *4*311*2## / *4*311*#2## pair
behind OWNd OpenWebNet-HA#137 and probe_area0 behind OWNd OpenWebNet-HA#141 / MyHOME OpenWebNet-HA#798; and one
sentence says reply_time_s is measured while tx.t is reconstructed from the
probe log.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the feat/config-flow-identify-manual-gateway branch from af65f3b to 95ab6ff Compare October 24, 2026 15:51
GreenGrassBlueOcean pushed a commit that referenced this pull request Oct 24, 2026
MyHomeServer1 README: "OWnd" typo; the alarm gate, the config-flow
identification and the liveness probe rows name MyHOME #803, #804, #806.
F461 README: the "What this capture backs" table extends to MyHOME #800-#806
(#802 the dimension 27 wire form, #803 *#5*0##, #804 the WHO 13 / 1013
identification replies, #806 the *#13**0## echo; #800 / #801 / #805 have no
frame in this capture); it states that f461_live_2026-10-15 holds the
dimension 22 reboot behind OWNd #136, the *4*311*2## / *4*311*#2## pair
behind OWNd #137 and probe_area0 behind OWNd #141 / MyHOME #798; and one
sentence says reply_time_s is measured while tx.t is reconstructed from the
probe log.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the feat/config-flow-identify-manual-gateway branch 2 times, most recently from acf222c to 502a0b0 Compare October 24, 2026 19:43

@GreenGrassBlueOcean GreenGrassBlueOcean 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.

Strong protocol-first direction: probing strictly in the #420 order (*#13**15## → *#1013**1## only on shared 200 → *#13**12## only behind fail-closed MAC_REQUEST_SAFE_MODELS) protects H4684 (020054) and MH200 (020102) from the *#13**12## daemon crash while keeping legacy gateways away from WHO=1013. Keeping this in Draft until OpenWebNet-HA/own-firmware-oracle#59 (and its eth0 MAC determinism prerequisite #57) lands is also the right call.

Requesting changes for a few correctness, safety, and config-flow issues before undrafting (inline comments attached on each hunk):

Blocking / High

  1. _async_apply_gateway_identity bypasses resolve_gateway_identity() and breaks three identity invariants (config_flow_discovery.py:532-541):
    • Overwrites valid brand/family selections (H4890 / LN4890 → AM4890 on WHO=1013 30; 003598 → F454) because reading.compatible_with(gateway.model_name) is not consulted.
    • Allows uncertain Nmap third-party WHO=13 codes (certain=False in WHO13_THIRD_PARTY_DEVICE_TYPES) to override an explicit manual model choice.
    • Does not persist "model_source": IDENTIFICATION_WHO13 in _new_entry_data, so runtime diagnostics later misreports source: "manual", configured_model: "F461".
  2. Unvalidated / all-zero MAC replies overwrite valid user-entered MACs (config_flow_discovery.py:521-531):
    • OWNGatewayEvent.mac_address formats any >= 6 integer tuple via f"{int(v):02x}" without 0..255 range checks or zero/broadcast filtering. A 6-zero reply (*#13**12*0*0*0*0*0*0##), broadcast (ff:ff:ff:ff:ff:ff), or out-of-range octet (> 255) overwrites a real user-typed MAC and collides with the "00:00:00:00:00:00" UI sentinel in async_step_user.

Medium

  1. Second manual gateway with default placeholder MAC 00:03:50:00:00:00 still aborts before identify_gateway can run (config_flow.py:308-312):
    • If Gateway 1 was outside MAC_REQUEST_SAFE_MODELS (e.g. F454 or MH202) and kept unique_id = "00:03:50:00:00:00", adding Gateway 2 (even an F461 or MyHomeServer1 3.87.13 whose real MAC would be learned after auth) with the default 00:03:50:00:00:00 aborts immediately in async_step_custom_manual before authentication/identification runs.
  2. Split-auth gateways (open monitor *99*1##, password-protected command *99*0##) emit WARNING + ERROR logs and skip identification (config_flow_discovery.py:99-109).

Polish, Docs & PR Description

  1. Refresh self.context / title_placeholders and gateway._log_id after _async_apply_gateway_identity (config_flow_discovery.py:456-460), and reuse WHO13_SHARED_DEVICE_TYPES instead of duplicating WHO13_SHARED_CODE = "200" (config_flow_discovery.py:67-80).
  2. own-firmware-oracle#59 table citation in PR description: Both Hardware Verification and What does the Firmware Oracle / Encyclopedia say interpret the — cells for F453, F453AV, F459, F460, F461, MH200N, MyHomeServer1 in findings/who13-identification.md as "ACK, no bus output". In #59, those 7 columns contain — across all 9 rows (including *#13**15##, *#13**16##, and *#1013**1##, which all 12 targets answer with out frames in published trace-corpus.tsv) — those columns were simply elided in the local markdown table while #57/#59 prose records the actual split (F453/F453AV/MH200N ACK with no output; F459/F460/F461/MyHomeServer1 output *#13**12*0*0*0*0## from factory config; F450/F454/MH202 output interface MAC; H4684/MH200 crash). Also check off - [x] **V2 Architecture:** N/A, config flow only. in the checklist.
  3. Documentation (docs/configuration/gateway-identification.md): Update gateway-identification.md to document setup-time manual probing and the MAC_REQUEST_SAFE_MODELS crash gate (and align line 84's *#1013*0*1## example with *#1013**1##).

Comment on lines +532 to +541
reading = read_who1013(code) if code else (read_who13(code13) if code13 and code13 != WHO13_SHARED_CODE else None)
if reading is not None and reading.known and reading.canonical != gateway.model_name:
LOGGER.info(
"Gateway at %s reports %s; using it instead of the selected model %s",
gateway.address,
reading.describe(),
gateway.model_name,
)
gateway.model_name = gateway.model = reading.canonical
gateway.profile = get_gateway_profile(gateway.model_name, gateway.model_number)

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.

[High] Delegate model resolution to resolve_gateway_identity() and persist model_source

Re-implementing model selection inline via reading.known and reading.canonical != gateway.model_name diverges from identity.resolve_gateway_identity() in three ways:

  1. Clobbers valid brand/family choices (compatible_with ignored): SUPPORTED_GATEWAY_MODELS explicitly offers "AM4890", "H4890", and "LN4890" in custom_manual. All three map to WHO=1013 code "30" (WHO1013_OBJECT_MODELS["30"] = ("AM4890", "H4890", ..., "LN4890", "LN4890A"), canonical == "AM4890"). Selecting "H4890" (confirmed on physical hardware in #466 to answer 200 → 30) or "LN4890" — or entering "003598" for F454 / lowercase "myhomeserver1" — gets overwritten with "AM4890" / "F454", whereas resolve_gateway_identity() preserves it because inband.compatible_with(configured) is True.
  2. Allows uncertain Nmap WHO=13 codes (certain=False) to override manual choices: Codes in WHO13_THIRD_PARTY_DEVICE_TYPES (12, 15, 16, 23, 27, 44, 51) return known=True, certain=False from read_who13(). Per identity.py and docs/configuration/gateway-identification.md, unverified codes may label an untrusted/Generic entry or corroborate, never contradict an explicit manual model.
  3. Omits "model_source" in _new_entry_data: Unlike GatewayIdentityMixin._apply_model() (which sets new_data["model_source"] = IDENTIFICATION_WHO13), mutating gateway.model_name without recording "model_source" causes identification_source to fall back to IDENTIFICATION_MANUAL, so diagnostics (identification()) misreports source: "manual", configured_model: "F461". (MyhomeOptionsFlowHandler already resets model_source = IDENTIFICATION_MANUAL if the user later edits it.)

Passing GatewayIdentityEvidence(manual=None if str(gateway.model_name or "").strip().lower() in ("", "generic", "gateway", "unknown") else gateway.model_name, who13_code=code13, who1013_code=code) to resolve_gateway_identity() handles all three cases cleanly and returns resolution.source for _new_entry_data["model_source"]. Also update gateway._log_id = f"[{gateway.model_name} gateway - {gateway.host}]" when the model changes.

Comment on lines +521 to +531
reported_mac = dr.format_mac(mac) if mac else None
if reported_mac and reported_mac != dr.format_mac(str(gateway.serial)):
LOGGER.info(
"Gateway at %s reports MAC %s; using it instead of the entered %s",
gateway.address,
reported_mac,
gateway.serial,
)
gateway.serial = reported_mac
await self.async_set_unique_id(reported_mac, raise_on_progress=False) # type: ignore[attr-defined]
self._abort_if_unique_id_configured() # type: ignore[attr-defined]

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.

[High] Validate mac and reject all-zero / broadcast / out-of-range replies before overwriting user input

OWNGatewayEvent (OWNd/message/gateway.py) formats any len(dimension_value) >= 6 integer tuple via f"{int(v):02x}" without 0..255 bounds checks or zero/broadcast filtering.

Emulated factory images in own-firmware-oracle#57 / #59 only return None because they output 4 zeros (*#13**12*0*0*0*0##, len < 6). If a gateway/config outputs 6 zeros (*#13**12*0*0*0*0*0*0##), broadcast (255*...*255), or an out-of-range integer (> 255, where dr.format_mac("100:00:00:00:00:00") returns the invalid string unchanged), reported_mac is truthy and overwrites a valid MAC typed by the user. In addition, "00:00:00:00:00:00" is the sentinel key for "Custom (IP Gateway)" in config_flow.py (async_step_user) and causes collisions across unconfigured gateways.

Validate mac via MACAddress(mac) and reject {"00:00:00:00:00:00", "ff:ff:ff:ff:ff:ff"} before accepting reported_mac.

Comment thread custom_components/myhome/config_flow.py Outdated
Comment on lines 308 to 312
await self.async_set_unique_id(user_input["serialNumber"], raise_on_progress=False)
self._abort_if_unique_id_configured()
# Typed by hand: once authenticated, ask the gateway for its MAC and model.
self._identify_after_auth = True
return await self.async_step_test_connection()

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.

[Medium] Pre-auth _abort_if_unique_id_configured() blocks adding a second manual gateway when an existing manual entry kept placeholder 00:03:50:00:00:00

From the PR problem statement: "a placeholder MAC stays the unique_id forever and a second gateway with the same placeholder cannot be added."

If Gateway 1 is any model outside MAC_REQUEST_SAFE_MODELS (F454, MH202, MH200N, MH200, H4684, or a MyHomeServer1 on older 2.82.06 firmware) added manually with the default 00:03:50:00:00:00, Gateway 1 keeps unique_id = "00:03:50:00:00:00". When the user later adds Gateway 2 — even a physical F461 2.0.11 or MyHomeServer1 3.87.13 whose real MAC would be learned after auth — and leaves the default 00:03:50:00:00:00 on the custom_manual form, self._abort_if_unique_id_configured() aborts immediately before async_step_test_connection / async_step_password / identify_gateway can run.

Deferring async_set_unique_id + _abort_if_unique_id_configured() on the manual path until right after _async_apply_gateway_identity in async_step_test_connection (running unconditionally on the final dr.format_mac(gateway.serial)) lets supported gateways resolve their real MAC while still aborting true duplicates.

Comment on lines +99 to +109
session = OWNCommandSession(gateway=gateway, logger=LOGGER)

async def _ask(request: typing.Any) -> list[typing.Any]:
replies = await session.send(request, is_status_request=True)
return replies if isinstance(replies, list) else []

try:
async with asyncio.timeout(IDENTIFY_TIMEOUT):
res = await session.connect()
if not (isinstance(res, dict) and res.get("Success")):
return None, None, None

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.

[Medium] Split-auth gateways (open monitor *99*1##, password-protected command *99*0##) emit WARNING + ERROR logs and skip identification

OWNSession.test_connection() negotiates *99*1## (monitor session), whereas identify_gateway() negotiates *99*0## (OWNCommandSession). On gateways configured with IP-whitelist / unauthenticated monitor access while requiring an OPEN/HMAC password on *99*0##:

  1. test_connection() returns {"Success": True} with gateway.password = None (skipping async_step_password).
  2. session.connect() here fails with password_required, and because LOGGER is passed directly to OWNCommandSession, OWNd emits both a WARNING (Connection requires a password, none was provided.) and an ERROR (Command session negotiation failed (password_required); giving up.) during a successful setup, re-introducing password_required log noise and skipping identification.

Consider either routing res.get("Message") == "password_required" on the command probe to async_step_password (since runtime command workers will need that password anyway), or passing a muted/debug logger to the best-effort OWNCommandSession probe.

Comment on lines +456 to 460
if getattr(self, "_identify_after_auth", False):
self._identify_after_auth = False
await self._async_apply_gateway_identity(gateway)

_new_entry_data = {

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.

[Low] Refresh self.context (CONF_NAME, CONF_MAC, title_placeholders) after _async_apply_gateway_identity

self.context is populated at lines 391–402 before _async_apply_gateway_identity(gateway) updates gateway.serial and gateway.model_name. When self._bus_primaries() is truthy and the flow proceeds to async_step_bus_topology(), self.context and title_placeholders retain the stale placeholder MAC and uncorrected model.

Comment on lines +67 to +80
WHO13_SHARED_CODE = "200"


def mac_request_is_safe(who13_code: str | None, who1013_code: str | None = None) -> bool:
"""Whether every model the reported codes can name is known to answer *#13**12##.

The WHO=1013 code decides when present (it is only asked on the shared code 200);
otherwise the WHO=13 device type does.
"""
if who1013_code:
models = read_who1013(who1013_code).models
elif who13_code and who13_code != WHO13_SHARED_CODE:
models = read_who13(who13_code).models
else:

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.

[Low] Reuse WHO13_SHARED_DEVICE_TYPES / read_who13(who13_code).shared instead of duplicating WHO13_SHARED_CODE = "200"

const.WHO13_SHARED_DEVICE_TYPES (exposed via read_who13(code).shared) already defines the set of shared WHO=13 codes; reusing it keeps a single source of truth.

Comment on lines +42 to +60
async def test_manual_flow_corrects_mac_and_model_from_the_gateway(hass: HomeAssistant, caplog) -> None:
test_connection = AsyncMock(
side_effect=[{"Success": False, "Message": "password_required"}, {"Success": True}]
)
identify = AsyncMock(return_value=("00:03:50:00:07:77", "200", "134"))

with caplog.at_level(logging.DEBUG, logger="custom_components.myhome"):
result = await _manual_flow(hass, test_connection, identify)

assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"]["mac"] == "00:03:50:00:07:77"
assert result["data"]["name"] == "F461"
assert result["title"] == "F461 Gateway"
assert result["result"].unique_id == "00:03:50:00:07:77"
identify.assert_awaited_once()
# The password probe is the normal path, not a failure worth a WARNING
assert not [
r for r in caplog.records if r.levelno >= logging.WARNING and "password_required" in r.getMessage()
]

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.

[Tests] Add regression coverage for brand variants, uncertain WHO=13 codes, bogus MACs, second-gateway placeholder, and worker defaults

Please add cases covering the edge cases above:

  1. Compatible brand variant kept: modelName="H4890" (or "LN4890") with identify_gateway -> (None, "200", "30") keeps "H4890" rather than overwriting with "AM4890".
  2. Uncertain WHO=13 code: identify_gateway -> (None, "44", None) labels "Generic" → "MH200N", while leaving an explicit manual "MH202" unchanged.
  3. Bogus MAC rejection: *#13**12*0*0*0*0*0*0## (00:00:00:00:00:00) and *#13**12*255*255*255*255*255*255## (ff:ff:ff:ff:ff:ff) leave the user-entered MAC intact.
  4. Second gateway with default placeholder MAC: With an existing manual F454 entry holding unique_id="00:03:50:00:00:00", adding an F461 with the default 00:03:50:00:00:00 completes auth, learns 00:03:50:00:07:77, and creates the entry instead of aborting early.
  5. Worker count recalculation: MyHomeServer1 (default 2 workers) corrected to MH200 (code13="4") or H4684 (code13="13") produces result["options"]["worker_count"] == 1 (currently both MyHomeServer1 and F461 default to 2, so command_session_default changing is not exercised).

gdluck and others added 8 commits October 25, 2026 00:02
…tion

After the password is accepted ask the gateway who it is in the OpenWebNet-HA#420
order: `*OpenWebNet-HA#13**15##` first, `*#1013**1##` only on the shared code 200,
and the MAC (`*OpenWebNet-HA#13**12##`) only from a model known to answer it, since
real H4684 and MH200 daemons crash on that frame. A typed placeholder
MAC or model is corrected from the reply and the unique_id re-set; the
`password_required` probe answer is logged at DEBUG. Confirmed live on
an F461 2.0.11.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y_identity

The manual flow re-implemented the model choice inline and so broke three
invariants the runtime resolver keeps: a brand variant of the reported model
(H4890 / LN4890 on WHO 1013 code 30, 003598 on 51) was overwritten by the
canonical name, an uncertain Nmap WHO 13 code could override an explicit
choice, and the entry never recorded `model_source`, so diagnostics reported
`manual` for a corrected model. The flow now hands the typed model (none for
Generic / Gateway / Unknown), the WHO 13 code and the WHO 1013 code to
`resolve_gateway_identity()`, persists `resolution.source` as `model_source`
and refreshes the OWNd profile and log id with the model.

Review follow-up (review 5639869140, point 1).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d one

OWNd formats any six integers of `*OpenWebNet-HA#13**12*a*b*c*d*e*f##` without a range
check, so an octet above 255 yields a 13-digit string, a factory
configuration answers six zeros (`00:00:00:00:00:00`, also the sentinel of
the first form's "Custom (IP Gateway)" choice) and nothing stops a
broadcast address. Such a value replaced a real MAC the user typed and
became the unique_id. The reply now has to pass the form's `MACAddress`
validation and must not be the all-zero or broadcast address; otherwise the
typed MAC stays and the reply is logged at DEBUG.

Review follow-up (review 5639869140, point 2).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… its MAC

The manual step set the unique_id from the typed MAC and aborted
`already_configured` before the connection test, so a second gateway typed
with the form's placeholder `00:03:50:00:00:00` was refused whenever an
earlier manual entry (an F454 or MH202, whose MAC is never asked) kept
that placeholder, although the new gateway would have reported its real
MAC right after authentication. The unique_id is now set once, in the
connection test after `_async_apply_gateway_identity`, from the final
`dr.format_mac(gateway.serial)`; a true duplicate still aborts there.

Review follow-up (review 5639869140, point 3).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mands

A gateway with an open monitor session (`*99*1##`, IP whitelist) and a
password-protected command session (`*99*0##`) passed the connection test
without a password, and the identification probe then answered
`password_required` while OWNd logged a WARNING and an ERROR in the middle
of a successful setup. `identify_gateway` now returns a `GatewayIdentity`
named tuple whose `password_required` flag routes the manual flow to the
password step (the runtime command workers need that password anyway) and
probes again with it; a second refusal is treated as no answer, so the
flow cannot loop. The probe runs on `PROBE_LOGGER`, which records every
OWNd line at DEBUG: the probe is best effort and its refusals are expected.

Review follow-up (review 5639869140, point 4).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… reuse the shared-code table

The flow context and its title placeholders were filled before the gateway
reported its MAC and model, so the bus topology step that follows when
another IP gateway exists showed the typed placeholder and the uncorrected
model; they are refreshed after `_async_apply_gateway_identity`. The
duplicated `WHO13_SHARED_CODE = "200"` goes: `read_who13(code).shared`
(`WHO13_SHARED_DEVICE_TYPES`) is the single source of truth. A test pins
that a MyHomeServer1 corrected to an MH200 (`*OpenWebNet-HA#13**15*4##`) or an H4684
(`*OpenWebNet-HA#13**15*13##`) starts one command worker.

Review follow-up (review 5639869140, point 5).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…st gate

`gateway-identification.md` gains a section on what the manual config flow
asks once the password is accepted (`*OpenWebNet-HA#13**15##`, `*#1013**1##` on a shared
code, `*OpenWebNet-HA#13**12##` only behind the fail-closed `MAC_REQUEST_SAFE_MODELS`
gate, since real H4684 and MH200 daemons crash on it), how the MAC reply
is validated, how the model goes through `resolve_gateway_identity`, and
the split-authentication case. The `*#1013*0*1##` example now reads
`*#1013**1##`, the frame the integration sends.

Review follow-up (review 5639869140, point 7).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…h a registered logger

Two points from an independent review of the follow-up diff. The password
form accepts an empty string, and `_async_apply_gateway_identity` returned
"ask for a password" whenever the command session said `password_required`
with no password set, so a blank answer brought the form back forever; the
flow now asks once (`_command_password_asked`) and treats a second refusal
as no answer. `PROBE_LOGGER` was a `logging.Logger` subclass built outside
the manager, so `logging.getLogger(PROBE_LOGGER.name)` (and
`caplog.set_level(..., logger=...)`) addressed a different object; it is
now `LOGGER.getChild("identify")` with a filter that lowers every record
to DEBUG.

Review follow-up (review 5639869140, points 4 and 5, second opinion).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck force-pushed the feat/config-flow-identify-manual-gateway branch from 502a0b0 to 652995b Compare October 24, 2026 21:53
@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

All seven points are on the branch (head 652995b, one commit per finding, rebased on the current base, still draft until OpenWebNet-HA/own-firmware-oracle#59 lands). An independent second-opinion review of the diff added two more that are in as well: a blank command password could have looped the password form (now asked once), and the muted probe logger is a registered child logger with a level-rewriting filter instead of an unmanaged Logger subclass.

  1. Model resolution (7864ba6): _async_apply_gateway_identity delegates to resolve_gateway_identity() with GatewayIdentityEvidence(manual=<typed model, None for Generic/Gateway/Unknown>, who13_code, who1013_code): H4890 / LN4890 stay on 200 → 30, 003598 stays on 51, uncertain code 44 labels a Generic entry MH200N but leaves a chosen MH202, model_source = resolution.source is persisted, profile and log_id refreshed. Tests for each case.
  2. MAC validation (6a8dfde): a reported MAC must pass the form's MACAddress validation and is refused when all-zero or broadcast: *#13**12*0*0*0*0*0*0##, *#13**12*255*…*255## and an octet of 256 all leave the typed MAC and the unique_id intact.
  3. Deferred duplicate check (27ed535): on the manual path the unique_id is set once, after identification, from the final dr.format_mac(gateway.serial); an existing manual F454 holding the placeholder no longer blocks an F461 typed with the same placeholder (it learns 00:03:50:00:07:77 and the entry is created); true duplicates still abort already_configured after authentication.
  4. Split authentication (83cb5d4, 652995b): identify_gateway returns GatewayIdentity(mac, who13_code, who1013_code, password_required); a password_required from the command probe routes to the password step once and the probe re-runs with the password; the probe logs through a registered child logger at DEBUG, so no WARNING / ERROR during a successful setup.
  5. Polish (ac7b2a5): flow context and title_placeholders refreshed after identification (pinned through the bus-topology step); WHO13_SHARED_CODE replaced by read_who13(code).shared; worker-count test: a MyHomeServer1 corrected to MH200 (4) or H4684 (13) starts one command worker.
  6. Description: the — cells of the oracle findings table are elided columns, not "ACK, no bus output"; the real split from the Home assistant 2022.8.0 and Integration stop working after reboot OS #57 / Problems loading lights after 2022.8 #59 prose is now stated (F453 / F453AV / MH200N ACK with no output; F459 / F460 / F461 / MyHomeServer1 *#13**12*0*0*0*0##; F450 / F454 / MH202 the interface MAC; H4684 / MH200 crash), with the live F461 2.0.11 and MyHomeServer1 3.87.13 real-MAC answers from the merged test(trace): ingest live F461 2.0.11 2026-10-23 capture and MyHomeServer1 3.87.13 reads behind OWNd #136-#142 and MyHOME #791-#798 #799 fixtures; the V2 checklist item is ticked; cross-repo references qualified.
  7. Docs (e73e8a1): gateway-identification.md gains the setup-time section (probe order, fail-closed MAC_REQUEST_SAFE_MODELS gate, MAC validation, resolver verdicts, split authentication); the *#1013*0*1## example reads *#1013**1##.

Full suite with plain OWNd master 4552 passed at 100 % coverage; ruff, typing ratchet, HA standards, docs sync and translations clean; test_config_flow_identify.py 29 cases (was 9).

@GreenGrassBlueOcean GreenGrassBlueOcean 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 — all 7 review findings from #5639869140 (plus the single-prompt guard on blank command passwords and the registered child logger filter) are cleanly addressed and covered by 29 regression cases at 100% statement coverage.

@GreenGrassBlueOcean
GreenGrassBlueOcean marked this pull request as ready for review October 25, 2026 21:19
@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit a27b32f into OpenWebNet-HA:v2-phase1-architecture Oct 25, 2026
17 checks passed
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 26, 2026
…ile_for_model (OpenWebNet-HA#804 follow-up)

_async_apply_gateway_identity called get_gateway_profile(model,
gateway.model_number): OWNd's signature is (model_name, firmware_version),
and model_number is always None on the manual path, so the second argument
was inert and read as if a model number were a firmware. The runtime resolves
the same correction through _profile_for_model (OpenWebNet-HA#818), which also maps the
SSDP spellings; the flow now uses it, without a firmware, which is not known
before the first connection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GreenGrassBlueOcean added a commit that referenced this pull request Oct 26, 2026
…e-arg-and-log

fix(config_flow,legacy_yaml): drop an inert profile argument, say what moved, no unreachable MAC fallback (#804 / #798 / #801 / #808 follow-up)
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.

3 participants