Skip to content

Commit 6f1a0b0

Browse files
author
Maksym Shtembuliak
committed
fix: support explicit routed collector discovery
Add bounded known-IP scan targets and preserve advertised NAT callback settings during identity checks. Correct issue #6 device attribution, document setup paths, and cover UDP payloads and native HA flows.
1 parent bfa08af commit 6f1a0b0

27 files changed

Lines changed: 484 additions & 56 deletions

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ the GitHub release body should be rendered from the matching version section her
99

1010
### Added
1111

12+
- Advanced setup can include up to eight known collector IPs in the ordinary
13+
scan, alongside local discovery. Routed/VPN collectors no longer require a
14+
hardcoded scan target. The list is setup-flow scoped; identity and recovery
15+
checks remain mandatory.
16+
1217
- Added a separate read-only **EyeBond 09C1 family** profile, qualified against
1318
the manufacturer's map and ZL Power GSIII captures (#50). It reports input
1419
and output voltage/frequency separately, battery voltage, load percentage,
@@ -52,6 +57,13 @@ the GitHub release body should be rendered from the matching version section her
5257

5358
### Fixed
5459

60+
- Manual callback identity checks now honor the configured advertised callback
61+
IP and port, separately from Home Assistant's local listener. NAT overrides
62+
no longer apply only to later recovery/runtime requests.
63+
- Corrected the issue #6 catalog attribution: PI30 captures from Sumry devices
64+
do not confirm Yingfa YF6.2K-2K-LEL-IF support. That model is now unresolved;
65+
generic PI30 support and the independently evidenced issue #27 model remain.
66+
5567
- Keep the configured callback IP when local-interface enumeration fails (#52).
5668
The default-route address is no longer treated as a complete interface list;
5769
existing BusyBox and collector-subnet discovery remain unchanged.

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ combines broadcast replies, already connected collectors, and a local `/24`
191191
unicast fallback when broadcast discovery is not enough. On a larger network,
192192
use the correct subnet broadcast or enter a known collector address through
193193
advanced setup instead of expecting every address in a `/16` to be probed.
194+
For a routed subnet, **Add known collector IPs to scan** supplements local
195+
discovery with explicitly entered addresses. Use manual setup when the collector
196+
needs a different callback address or port behind NAT.
194197

195198
<p align="center"><img src="docs/images/setup-02-scanning.png" alt="Scanning the local network" width="480"></p>
196199

‎catalog/inverter_models/models/yingfa_yf6_2k_2k_lel_if.json‎

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,36 @@
44
"manufacturer": "Yingfa",
55
"model": "YF6.2K-2K-LEL-IF",
66
"aliases": [
7-
"YF6.2K",
8-
"PI30 6200"
7+
"YF6.2K"
98
],
10-
"lifecycle": "supported",
9+
"lifecycle": "research",
1110
"variants": [
1211
{
13-
"variant_key": "pi30_max_qpiri",
14-
"label": "PI30 MAX 6200W raw-ASCII runtime over SmartESS DTU AT collector",
15-
"device_descriptor_keys": [
16-
"pi30_max_qpiri"
17-
],
18-
"known_firmware": [
19-
"VERFW:00074.11",
20-
"VERFW2:00000.00"
21-
]
12+
"variant_key": "unidentified",
13+
"label": "Protocol and runtime mapping not yet confirmed",
14+
"device_descriptor_keys": []
2215
}
2316
],
2417
"validation": {
25-
"hardware": "captured",
26-
"telemetry": "confirmed",
27-
"controls": "partial"
18+
"hardware": "reported",
19+
"telemetry": "unknown",
20+
"controls": "none"
2821
},
2922
"coverage": {
30-
"runtime_control_surface": "available",
31-
"smartess_control_surface": "cloud_catalog",
23+
"runtime_control_surface": "none",
24+
"smartess_control_surface": "unknown",
3225
"vendor_register_map": "unknown",
3326
"notes": [
34-
"The proxy capture proves PI30 ASCII telemetry and cloud-visible PI30 controls, but Home Assistant write behavior is not yet independently user-confirmed.",
35-
"This collector family uses raw PI30 ASCII payloads directly over the SmartESS DTU AT callback stream rather than the legacy EyeBond binary FC=4 forwarding tunnel."
27+
"The earlier PI30 capture was attributed to this model in error. The owner clarified that the captured devices were Sumry inverters, not this Yingfa."
3628
]
3729
},
3830
"known_limitations": [
39-
"The commercial model name is known from the user report, not from a local inverter identity command in the captured traffic.",
40-
"QMN/model-number evidence was not present in the cloud proxy capture, so runtime resolution is currently by PI30 protocol plus QPIRI 28-field MAX-family shape."
31+
"No confirmed inverter protocol or runtime mapping is available for this particular model.",
32+
"Working collector discovery or a UDP callback acknowledgement does not establish inverter compatibility."
4133
],
42-
"knowledge_summary": "Yingfa YF6.2K-2K-LEL-IF support is based on issue #6 and a private proxy capture. The inverter speaks PI30 ASCII through a SmartESS DTU AT collector; QPI reports PI30, QPIRI reports a 28-field 6200W/48V MAX-style rating, and QPIGS returns live telemetry. The integration should use the PI30 MAX runtime surface over raw AT-text payload transport for this collector family.",
34+
"knowledge_summary": "The issue #6 owner reports a Yingfa YF6.2K-2K-LEL-IF, but its inverter protocol remains unresolved. Previously linked PI30 evidence belongs to Sumry devices and is not proof of support for this Yingfa. This correction does not remove generic PI30 support or evidence for other independently reported Yingfa models.",
4335
"source_keys": [
4436
"github_issue_6",
45-
"support_archive_issue_6_20260622_proxy_capture"
37+
"issue_6_device_attribution_correction"
4638
]
4739
}

‎catalog/inverter_models/schemas/model.schema.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@
4242
"label": {"type": "string", "minLength": 1},
4343
"device_descriptor_keys": {
4444
"type": "array",
45-
"minItems": 1,
45+
"minItems": 0,
4646
"items": {"type": "string"},
47-
"description": "Keys of devices[] entries in the runtime inverter catalog."
47+
"description": "Keys of devices[] entries in the runtime inverter catalog. Empty only for an unresolved research variant; the catalog validator enforces this lifecycle boundary."
4848
},
4949
"validation": {
5050
"type": "object",

‎catalog/inverter_models/sources/github_issue_6.json‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66
"reference": "project-issue:6",
77
"captured_at": "2026-06-22",
88
"visibility": "public",
9-
"summary": "Device owner reported a Yingfa YF6.2K-2K-LEL-IF inverter behind an EyeBond logger. The local network topology initially masked the callback source address, but routing/NAT diagnostics later confirmed that the collector could call Home Assistant back with its real remote address.",
9+
"summary": "Device owner reported an unresolved Yingfa YF6.2K-2K-LEL-IF behind an EyeBond logger across routed networks. Collector connectivity observations are not evidence of a specific inverter protocol. The later device-attribution correction separates Sumry captures from this Yingfa report.",
1010
"assertions": [
11-
"commercial_identity",
12-
"runtime_compatibility"
11+
"commercial_identity"
1312
]
1413
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"schema_version": 1,
3+
"source_key": "issue_6_device_attribution_correction",
4+
"kind": "private_message",
5+
"title": "Issue #6 device attribution correction",
6+
"reference": "private:issue-6-device-attribution-correction-20260930",
7+
"captured_at": "2026-09-30",
8+
"visibility": "private",
9+
"summary_publishable": true,
10+
"summary": "The owner clarified that the PI30 evidence in this case concerns Sumry inverters, reporting 6.2 kW and 4.2 kW units, while the Yingfa device remains unresolved. Exact Sumry commercial model names and per-device capture attribution still require confirmation; successful operation of both units, model-specific mappings and write validation are not inferred from the power ratings.",
11+
"assertions": ["commercial_identity"]
12+
}

‎catalog/inverter_models/sources/support_archive_issue_6_20260622_proxy_capture.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
"schema_version": 1,
33
"source_key": "support_archive_issue_6_20260622_proxy_capture",
44
"kind": "support_archive",
5-
"title": "Yingfa YF6.2K-2K-LEL-IF proxy capture",
5+
"title": "Issue #6 PI30 proxy capture — commercial attribution corrected",
66
"reference": "private:support-archive-issue-6-20260622-proxy-capture",
77
"captured_at": "2026-06-22",
88
"visibility": "private",
99
"summary_publishable": true,
10-
"summary": "Private proxy capture 01KVRFV6VETJ4B7MQ54DB5RDFF_20260622T211305079147Z.zip shows a SmartESS DTU AT collector using endpoint dtu_ess.eybond.com,18899,TCP and UART 2400,8,1,NONE. The cloud sends raw PI30 ASCII commands directly on the AT callback stream: QPI, QID, QSID, QVFW, QVFW2, QPIRI, QFLAG, QPIGS, QPIGS2, QMODI, QPIWS, QDI, QMCHGCR, QMUCHGCR, and QOPM. QPI returns PI30; QPIRI returns a 28-field 6200W/48V rating; QPIGS returns a 21-field live telemetry frame.",
10+
"summary": "Private capture shows raw PI30 ASCII over a SmartESS DTU AT collector: QPI returns PI30, QPIRI returns a 28-field 6200 W / 48 V rating, and QPIGS returns a 21-field live telemetry frame. These wire facts remain valid, but the earlier attribution to Yingfa YF6.2K-2K-LEL-IF is withdrawn following the owner's Sumry clarification. The capture does not establish an exact Sumry commercial model or validate a separate 4.2 kW unit.",
1111
"assertions": [
1212
"runtime_compatibility",
1313
"telemetry_validation",

‎custom_components/eybond_local/connection/callback_identity.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,10 @@ class CallbackIdentityRequest:
289289
# explicit bootstrap protocol selection (see OnboardingWireProbeIntent).
290290
# ``None`` keeps today's passive-evidence-only behavior.
291291
bootstrap_probe: OnboardingWireProbeIntent | ObservedSessionWireProbeIntent | None = None
292+
# Callback payload destination may differ from the local listener behind
293+
# NAT. These are validated user settings, never inferred from the TCP peer.
294+
advertised_server_ip: str = ""
295+
advertised_tcp_port: int = 0
292296

293297

294298
@dataclass(frozen=True, slots=True)
@@ -370,8 +374,8 @@ async def async_send(self, request: CallbackIdentityRequest) -> None:
370374

371375
await async_send_callback_trigger(
372376
bind_ip=request.server_ip,
373-
advertised_server_ip=request.server_ip,
374-
advertised_server_port=int(request.tcp_port),
377+
advertised_server_ip=request.advertised_server_ip or request.server_ip,
378+
advertised_server_port=int(request.advertised_tcp_port or request.tcp_port),
375379
target_ip=request.target_ip,
376380
udp_port=int(request.udp_port),
377381
timeout=DEFAULT_ONBOARDING_TIMEOUT_POLICY.discovery_timeout,

‎custom_components/eybond_local/flows/config/base.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ def __init__(self) -> None:
7474
self._default_broadcast = DEFAULT_DISCOVERY_TARGET
7575
self._interface_options: list[dict[str, str]] = []
7676
self._auto_config: dict[str, Any] = {}
77+
# Explicit scan intent only; never inferred from TCP peers or stored as
78+
# a second runtime route. Each selected collector still needs admission.
79+
self._scan_known_collector_ips: tuple[str, ...] = ()
7780
self._manual_defaults: dict[str, Any] = {}
7881
self._manual_config: dict[str, Any] = {}
7982
self._manual_result: OnboardingResult | None = None

‎custom_components/eybond_local/flows/config/manual.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ async def _async_run_manual_callback_attempt(
200200
old_session_id=identity_context.old_session_id,
201201
owner_prefix="callback_verification",
202202
bootstrap_probe=bootstrap_probe,
203+
advertised_server_ip=str(settings.get(CONF_ADVERTISED_SERVER_IP) or "").strip(),
204+
advertised_tcp_port=int(settings.get(CONF_ADVERTISED_TCP_PORT) or 0),
203205
),
204206
)
205207
if not outcome.identity_certified:

0 commit comments

Comments
 (0)