Repository navigation
feat(config_flow): identify a manually added gateway after authentication - #804
Conversation
ef7be41 to
af65f3b
Compare
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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>
af65f3b to
95ab6ff
Compare
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>
acf222c to
502a0b0
Compare
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
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
_async_apply_gateway_identitybypassesresolve_gateway_identity()and breaks three identity invariants (config_flow_discovery.py:532-541):- Overwrites valid brand/family selections (
H4890/LN4890→AM4890on WHO=101330;003598→F454) becausereading.compatible_with(gateway.model_name)is not consulted. - Allows uncertain Nmap third-party WHO=13 codes (
certain=FalseinWHO13_THIRD_PARTY_DEVICE_TYPES) to override an explicit manual model choice. - Does not persist
"model_source": IDENTIFICATION_WHO13in_new_entry_data, so runtime diagnostics later misreportssource: "manual", configured_model: "F461".
- Overwrites valid brand/family selections (
- Unvalidated / all-zero MAC replies overwrite valid user-entered MACs (
config_flow_discovery.py:521-531):OWNGatewayEvent.mac_addressformats any>= 6integer tuple viaf"{int(v):02x}"without0..255range 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 inasync_step_user.
Medium
- Second manual gateway with default placeholder MAC
00:03:50:00:00:00still aborts beforeidentify_gatewaycan run (config_flow.py:308-312):- If Gateway 1 was outside
MAC_REQUEST_SAFE_MODELS(e.g.F454orMH202) and keptunique_id = "00:03:50:00:00:00", adding Gateway 2 (even anF461orMyHomeServer13.87.13whose real MAC would be learned after auth) with the default00:03:50:00:00:00aborts immediately inasync_step_custom_manualbefore authentication/identification runs.
- If Gateway 1 was outside
- Split-auth gateways (open monitor
*99*1##, password-protected command*99*0##) emitWARNING+ERRORlogs and skip identification (config_flow_discovery.py:99-109).
Polish, Docs & PR Description
- Refresh
self.context/title_placeholdersandgateway._log_idafter_async_apply_gateway_identity(config_flow_discovery.py:456-460), and reuseWHO13_SHARED_DEVICE_TYPESinstead of duplicatingWHO13_SHARED_CODE = "200"(config_flow_discovery.py:67-80). own-firmware-oracle#59table citation in PR description: BothHardware VerificationandWhat does the Firmware Oracle / Encyclopedia sayinterpret the—cells forF453, F453AV, F459, F460, F461, MH200N, MyHomeServer1infindings/who13-identification.mdas "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 withoutframes in publishedtrace-corpus.tsv) — those columns were simply elided in the local markdown table while#57/#59prose records the actual split (F453/F453AV/MH200NACK with no output;F459/F460/F461/MyHomeServer1output*#13**12*0*0*0*0##from factory config;F450/F454/MH202output interface MAC;H4684/MH200crash). Also check off- [x] **V2 Architecture:** N/A, config flow only.in the checklist.- Documentation (
docs/configuration/gateway-identification.md): Updategateway-identification.mdto document setup-time manual probing and theMAC_REQUEST_SAFE_MODELScrash gate (and align line 84's*#1013*0*1##example with*#1013**1##).
| 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) |
There was a problem hiding this comment.
[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:
- Clobbers valid brand/family choices (
compatible_withignored):SUPPORTED_GATEWAY_MODELSexplicitly offers"AM4890","H4890", and"LN4890"incustom_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#466to answer200→30) or"LN4890"— or entering"003598"forF454/ lowercase"myhomeserver1"— gets overwritten with"AM4890"/"F454", whereasresolve_gateway_identity()preserves it becauseinband.compatible_with(configured) is True. - Allows uncertain Nmap WHO=13 codes (
certain=False) to override manual choices: Codes inWHO13_THIRD_PARTY_DEVICE_TYPES(12,15,16,23,27,44,51) returnknown=True, certain=Falsefromread_who13(). Peridentity.pyanddocs/configuration/gateway-identification.md, unverified codes may label an untrusted/Genericentry or corroborate, never contradict an explicit manual model. - Omits
"model_source"in_new_entry_data: UnlikeGatewayIdentityMixin._apply_model()(which setsnew_data["model_source"] = IDENTIFICATION_WHO13), mutatinggateway.model_namewithout recording"model_source"causesidentification_sourceto fall back toIDENTIFICATION_MANUAL, so diagnostics (identification()) misreportssource: "manual", configured_model: "F461". (MyhomeOptionsFlowHandleralready resetsmodel_source = IDENTIFICATION_MANUALif 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.
| 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] |
There was a problem hiding this comment.
[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.
| 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() |
There was a problem hiding this comment.
[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.
| 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 |
There was a problem hiding this comment.
[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##:
test_connection()returns{"Success": True}withgateway.password = None(skippingasync_step_password).session.connect()here fails withpassword_required, and becauseLOGGERis passed directly toOWNCommandSession,OWNdemits both aWARNING(Connection requires a password, none was provided.) and anERROR(Command session negotiation failed (password_required); giving up.) during a successful setup, re-introducingpassword_requiredlog 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.
| if getattr(self, "_identify_after_auth", False): | ||
| self._identify_after_auth = False | ||
| await self._async_apply_gateway_identity(gateway) | ||
|
|
||
| _new_entry_data = { |
There was a problem hiding this comment.
[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.
| 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: |
There was a problem hiding this comment.
[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.
| 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() | ||
| ] |
There was a problem hiding this comment.
[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:
- Compatible brand variant kept:
modelName="H4890"(or"LN4890") withidentify_gateway -> (None, "200", "30")keeps"H4890"rather than overwriting with"AM4890". - Uncertain WHO=13 code:
identify_gateway -> (None, "44", None)labels"Generic"→"MH200N", while leaving an explicit manual"MH202"unchanged. - 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. - Second gateway with default placeholder MAC: With an existing manual
F454entry holdingunique_id="00:03:50:00:00:00", adding anF461with the default00:03:50:00:00:00completes auth, learns00:03:50:00:07:77, and creates the entry instead of aborting early. - Worker count recalculation:
MyHomeServer1(default2workers) corrected toMH200(code13="4") orH4684(code13="13") producesresult["options"]["worker_count"] == 1(currently bothMyHomeServer1andF461default to2, socommand_session_defaultchanging is not exercised).
…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>
502a0b0 to
652995b
Compare
|
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.
Full suite with plain OWNd master 4552 passed at 100 % coverage; ruff, typing ratchet, HA standards, docs sync and translations clean; |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
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.
a27b32f
into
OpenWebNet-HA:v2-phase1-architecture
…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>
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, anddocs/configuration/gateway-identification.mdalready 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_requiredfrom 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
config_flow_discovery.py:identify_gateway(gateway)opens one command session on the authenticated gateway and returns aGatewayIdentity(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 inMAC_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 throughPROBE_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:MACAddressvalidation (six octets,0..255) and is neither00:00:00:00:00:00(the factory value, also the "Custom (IP Gateway)" sentinel of the first form) norff: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);identity.resolve_gateway_identity()withGatewayIdentityEvidence(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/LN4890on OBJECT_MODEL30,003598on51), an uncertain Nmap WHO 13 code may label aGenericentry but never overrides a chosen model, a certain code corrects it; the OWNd profile andlog_idfollow, andresolution.sourceis persisted asmodel_sourceso diagnostics reportwho13for a corrected entry;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_connectionsets the unique_id on the manual path only after identification, from the finaldr.format_mac(gateway.serial), then refreshes the flow context and title placeholders, then persistsmodel_source. A true duplicate still abortsalready_configured; a second gateway typed with the placeholder MAC gets to report its real one first.password_requiredfrom the test connection logged at DEBUG; other failures stay WARNING.config_flow.py: the manual step no longer sets the unique_id before authentication; it sets_identify_after_authand leaves the unique_id to the connection test.identify_gatewayandGatewayIdentityare re-exported for patching.identity.py:UNTRUSTED_MODEL_LABELS("",generic,gateway,unknown) names the labels that are no model, shared byidentification_sourceand the flow.docs/configuration/gateway-identification.md: a "Setup time" section documents the probe order, theMAC_REQUEST_SAFE_MODELScrash gate, the MAC validation, the resolver verdicts and the split-authentication case; the*#1013*0*1##example reads*#1013**1##.tests/test_config_flow_identify.py(new, 29 cases) and the three patched manual-flow tests intests/test_config_flow.py. Fixtures use192.0.2.92and00:03:50:00:07:77..github/release-notes/2.0.0b16.md: one line under "Other changes".MAC_REQUEST_SAFE_MODELSis 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)
resolve_gateway_identity(); brand variants kept, uncertain codes never override a manual choice,model_sourcepersisted, profile andlog_idrefreshed (tests:H4890/LN4890kept on200→30,003598kept on51, code44labelsGeneric→MH200Nbut leavesMH202,GenericlabelledF461from134,model_sourcewho13/manual).*#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 as100) all leave the user's MAC and unique_id intact.00:03:50:00:00:00no longer blocks an F461 typed with the same placeholder (it learns00:03:50:00:07:77and the entry is created); two gateways whose MAC is never learned still cannot share the placeholder (already_configuredafter authentication).test_form_already_configurednow patchesidentify_gatewayand asserts the abort happens after the probe.password_requiredlines at DEBUG throughPROBE_LOGGER; a blank password does not bring the form back.title_placeholdersrefreshed after identification (pinned through the bus topology step),WHO13_SHARED_CODEreplaced byread_who13(code).shared, worker-count test (MyHomeServer1corrected toMH200via*#13**15*4##orH4684via*#13**15*13##starts one command worker).MAC_REQUEST_SAFE_MODELSgate 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 forcaplog.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 }Hardware Verification
00:03:50:00:00:00and modelMyHomeServer1.user→custom→custom_manualwith 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:23in this text); HA loggedGateway at <host> reports MAC 00:03:50:00:10:23; using it instead of the entered 00:03:50:00:00:00and… reports WHO=1013 OBJECT_MODEL 134; using it instead of the selected model MyHomeServer1;create_entrytitledF461 Gateway, entry dataname: F461, diagnosticsidentification.model: F461, profileF461Profile; no WARNING forpassword_required. Adding it again abortsalready_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; onlymyhome.sweep_busand 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.*#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 intests/test_config_flow.py(test_form_already_configured,test_password_required_and_error,test_custom_manual_entry_manufacturer_type).model_sourceasserts); 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 toGatewayIdentity); polish 3 fail; second-opinion follow-up 2 fail. All pass after.master(f04d8aa, the behaviour of the 2.0.0b11 pin), rebased onv2-phase1-architecture798d82c: 4552 passed, 1 skipped, 100 % statement coverage (16,190 / 16,190).scripts/verify_ownd_coverage.py100 %,scripts/typing_ratchet.py0 errors (baseline 0),ruff check .,scripts/verify_ha_standards.py,scripts/sync_documentation.py --check,scripts/manage_translations.py checkall pass.HA Compatibility (beta)job failed ontests/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
tests/fixtures/traces/f461_live_2026-10-23/runrun1_oracle_replay_readonly, stepsoracle_who13andplant_reads(trace PR #799, merged)*#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 sessiontests/fixtures/traces/myhomeserver1_live_2026-10-24/runrun1_identify(trace PR #799, merged)*#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*#13**12##→nack, verdictcrashWhat does the Firmware Oracle / Encyclopedia say
results/<P>/<V>/oracle/full/trace-corpus.tsv,*#13**15##:ack/outon all 12 emulated gateways;200on F450, F454, F459, F460, F461, MH202, MyHomeServer1;0on F453, F453AV, MH200N;13on H4684;4on MH200. The basis for asking it first and for treating200as shared.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##. Itsfindings/who13-identification.mdtable 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##withoutframes in the publishedtrace-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 answerack/outwith 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.*#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
What is the problem,What is the solution,State Diagram,How is it tested,Where are the traces,What does the Firmware Oracle / Encyclopedia say).pytest,ruff, andscripts/verify_ha_standards.pypass.🤖 Generated with Claude Code