Skip to content

Commit 1deb44c

Browse files
author
Maksym Shtembuliak
committed
fix: correct model-specific inverter projections
1 parent 6c46c32 commit 1deb44c

20 files changed

Lines changed: 257 additions & 43 deletions

‎catalog/inverter_models/models/anenji_hhs_11kw_wifi_no_parallel.json‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@
2727
"notes": [
2828
"The exact layout/model fingerprint is captured from a beta.3 support archive.",
2929
"The compatible protocol 3-10 telemetry schema is enabled read-only.",
30+
"A later proxy capture and matching DESSMonitor export confirmed Grid Power at holding register 340 for this exact fingerprint.",
3031
"No ANJ-11KW writable capability is inherited until this exact HHS model is write-validated."
3132
]
3233
},
3334
"known_limitations": [
3435
"Local controls remain unavailable pending model-specific hardware validation."
3536
],
36-
"knowledge_summary": "An Anenji HHS 11kW non-parallel inverter with an exact captured protocol-3 fingerprint. The integration exposes the compatible telemetry schema read-only and deliberately withholds ANJ write controls.",
37+
"knowledge_summary": "An Anenji HHS 11kW non-parallel inverter with an exact captured protocol-3 fingerprint. The integration exposes its compatible telemetry schema read-only, including hardware-correlated Grid Power, and deliberately withholds ANJ write controls.",
3738
"source_keys": [
38-
"github_issue_13_hhs_11kw_beta3"
39+
"github_issue_13_hhs_11kw_beta3",
40+
"github_issue_13_hhs_grid_power_20260827"
3941
]
4042
}

‎catalog/inverter_models/models/kevolt_pd0080g_tpm_eu.json‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
"manufacturer": "Kevolt",
55
"model": "PD0080G-TPM-EU",
66
"aliases": [
7-
"Kevolt 80 kW Three-Phase Hybrid"
7+
"Kevolt 8 kW Three-Phase Hybrid"
88
],
99
"lifecycle": "experimental",
1010
"variants": [
1111
{
1212
"variant_key": "deye_3ph_high_80kw",
13-
"label": "Deye-compatible FC03 high-power map, protocol 0x0104, 80 kW",
13+
"label": "Deye-compatible FC03 high-power map, protocol 0x0104, 8 kW",
1414
"device_descriptor_keys": [
1515
"deye_3ph_high_80kw"
1616
],
@@ -21,7 +21,7 @@
2121
}
2222
],
2323
"validation": {
24-
"hardware": "captured",
24+
"hardware": "confirmed",
2525
"telemetry": "partial",
2626
"controls": "partial"
2727
},
@@ -31,8 +31,9 @@
3131
"vendor_register_map": "documented",
3232
"notes": [
3333
"The supplied SmartValue proxy capture contains repeated, CRC-valid Modbus FC03 request/response pairs for the Deye-compatible three-phase runtime map at registers 500-683.",
34-
"Runtime identification uses four local raw anchors: device type 5, protocol version 0x0104, and low-first rated-power words 0x3880/0x0001 (80000 raw W).",
35-
"The translated document labels the rated-power words as 0.1 W, but that scale conflicts with both the reported 80 kW hardware and its captured raw value 80000. Runtime therefore uses the hardware-correlated 1 W scale and fingerprints the unscaled words.",
34+
"Runtime identification uses four local raw anchors: device type 5, protocol version 0x0104, and low-first rated-power words 0x3880/0x0001 (80000 raw; documented 0.1 W scale = 8000 W).",
35+
"Direct hardware feedback confirms this is the 8 kW model, so the runtime now follows the protocol document's 0.1 W rated-power scale. The raw words remain the stable fingerprint anchors.",
36+
"Hardware feedback confirms the exposed battery values already use the expected negative-on-charge sign; the integration preserves that wire sign.",
3637
"Runtime reads use small register blocks of at most 15 words rather than the unsupported 500-599 bulk read reported in the issue.",
3738
"A bounded document-backed operational subset with unambiguous register encoding is available as 118 untested controls only in Full control mode. It covers battery, charging, generator, smart-load, export, metering, TOU, topology, and selected advanced functions.",
3839
"Settings read-back is rotated one compact block per poll so the expanded control surface does not delay live telemetry. Every write uses FC 0x10, preserves unrelated shared-register bits, and requires an immediate wire read-back before it is accepted."
@@ -41,16 +42,17 @@
4142
"known_limitations": [
4243
"All 118 document-backed controls are untested and disabled by default. Users must explicitly select Full control and then enable only the individual entities they intend to test.",
4344
"Factory reset, production-test, EEPROM initialization, calibration, BMS-owned live words, parallel-cluster addressing, and grid-code/certification curves are intentionally not exposed as user controls even though they occupy writable registers in the protocol document.",
44-
"Battery output power/current retain the documented wire sign as diagnostic values; their charge/discharge sign has not yet been promoted to canonical battery telemetry.",
45+
"Battery output power/current retain the hardware-confirmed wire sign; no additional canonical sign projection is inferred.",
4546
"Heat-sink temperature is diagnostic because the observed raw value requires the offset-1000 convention used by adjacent Deye storage maps, while the translated table omits that offset for register 541.",
46-
"The commercial Kevolt identity comes from the public issue report; the local fingerprint identifies the compatible 80 kW Deye-family wire layout and may be shared by OEM siblings.",
47+
"The commercial Kevolt identity comes from the public issue report; the local fingerprint identifies the compatible 8 kW Deye-family wire layout and may be shared by OEM siblings.",
4748
"A current-release hardware retest is still required before telemetry support can be marked confirmed."
4849
],
49-
"knowledge_summary": "Kevolt PD0080G-TPM-EU is a reported 80 kW three-phase hybrid inverter behind an EyeBond Wi-Fi collector. The latest SmartValue proxy capture proves a Modbus RTU FC03 layout with device type 5, protocol version 0x0104, rated-power words resolving to 80000 raw W, and live telemetry in the Deye-compatible 500-683 register range. Deye Modbus RTU V1.04 corroborates the telemetry and documents a broader FC 0x10 settings range. The exact wire fingerprint therefore exposes the current document-backed telemetry surface plus 118 opt-in, untested controls whose encodings are unambiguous, with shared-field-safe writes and mandatory immediate read-back confirmation.",
50+
"knowledge_summary": "Kevolt PD0080G-TPM-EU is a hardware-confirmed 8 kW three-phase hybrid inverter behind an EyeBond Wi-Fi collector. The latest SmartValue proxy capture proves a Modbus RTU FC03 layout with device type 5, protocol version 0x0104, rated-power words 80000 raw at the documented 0.1 W scale (8000 W), and live telemetry in the Deye-compatible 500-683 register range. Deye Modbus RTU V1.04 corroborates the telemetry and documents a broader FC 0x10 settings range. The exact wire fingerprint therefore exposes the current document-backed telemetry surface plus 118 opt-in, untested controls whose encodings are unambiguous, with shared-field-safe writes and mandatory immediate read-back confirmation.",
5051
"source_keys": [
5152
"github_issue_15",
5253
"proxy_capture_issue_15_20260811",
5354
"documentation_deye_three_phase_modbus_high_map",
54-
"maintainer_kevolt_control_readback_20260826"
55+
"maintainer_kevolt_control_readback_20260826",
56+
"maintainer_kevolt_hardware_feedback_20260827"
5557
]
5658
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"schema_version": 1,
3+
"source_key": "github_issue_13_hhs_grid_power_20260827",
4+
"kind": "github_issue",
5+
"title": "Anenji HHS-11kW Grid Power proxy and cloud correlation",
6+
"reference": "project-issue:13",
7+
"captured_at": "2026-08-27",
8+
"visibility": "public",
9+
"summary": "A proxy capture from the exact layout-3/model-29440 HHS variant showed the manufacturer cloud repeatedly reading holding register 340 as Grid Power. Two captured values, 1729 W and 1709 W, matched the corresponding DESSMonitor export exactly. The compatible family register 204 remained zero and is not the Grid Power source for this fingerprint.",
10+
"assertions": [
11+
"telemetry_validation",
12+
"register_map",
13+
"hardware_test"
14+
]
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"schema_version": 1,
3+
"source_key": "maintainer_kevolt_hardware_feedback_20260827",
4+
"kind": "private_message",
5+
"title": "Kevolt rated-power and battery-sign hardware feedback",
6+
"reference": "private:kevolt-hardware-feedback-20260827",
7+
"captured_at": "2026-08-27",
8+
"visibility": "private",
9+
"summary_publishable": true,
10+
"summary": "The hardware owner confirmed that the exact captured Kevolt PD0080G-TPM-EU unit is rated for 8 kW, not 80 kW, and that its battery current is negative while the battery is charging. Main telemetry and refresh behavior were reported as working on the maintainer test build.",
11+
"assertions": [
12+
"commercial_identity",
13+
"telemetry_validation",
14+
"hardware_test"
15+
]
16+
}

‎custom_components/eybond_local/drivers/smg.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,14 @@ async def async_read_values(
499499
config_block = await session.read_holding(config_block_start, config_block_count)
500500
values.update(_decode_block(config_block_start, config_block, config_fields))
501501
values.update(await _read_optional_specs(session, aux_config_fields))
502+
# Exact model overlays may correct a value inherited from a compatible
503+
# family schema. Remove the inherited projection before the optional
504+
# read so a failed model-specific read is reported as unavailable,
505+
# never as a plausible value from the wrong register.
506+
value_overrides = schema.spec_sets.get("value_overrides", ())
507+
for spec in value_overrides:
508+
values.pop(spec.key, None)
509+
values.update(await _read_optional_specs(session, value_overrides))
502510
missing_probe_details = await _read_missing_optional_probe_details(session, inverter)
503511
if missing_probe_details:
504512
inverter.details.update(missing_probe_details)

‎custom_components/eybond_local/metadata/compiled_detection_catalog.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,42 @@ def resolve_unique_persisted_model_surface(
332332
return descriptor, surface
333333

334334

335+
def model_names_share_catalog_identity(
336+
first_model_name: object,
337+
second_model_name: object,
338+
*,
339+
catalog: CompiledDetectionCatalog | None = None,
340+
) -> bool:
341+
"""Return whether two model labels resolve to one exact catalog identity.
342+
343+
Display names can change while an entry retains the previous name as a
344+
declared alias. Treat that migration as the same inverter only when both
345+
labels resolve uniquely to the same device descriptor and runtime surface.
346+
Unknown, malformed, family-fallback, and ambiguous labels fail closed.
347+
"""
348+
349+
if type(first_model_name) is not str or type(second_model_name) is not str:
350+
return False
351+
first = first_model_name.strip()
352+
second = second_model_name.strip()
353+
if not first or not second:
354+
return False
355+
if first == second:
356+
return True
357+
358+
resolved = catalog if catalog is not None else load_compiled_detection_catalog()
359+
first_identity = resolve_unique_persisted_model_surface(first, catalog=resolved)
360+
second_identity = resolve_unique_persisted_model_surface(second, catalog=resolved)
361+
if first_identity is None or second_identity is None:
362+
return False
363+
first_descriptor, first_surface = first_identity
364+
second_descriptor, second_surface = second_identity
365+
return (
366+
first_descriptor.key == second_descriptor.key
367+
and first_surface.key == second_surface.key
368+
)
369+
370+
335371
def compile_detection_catalog(
336372
source: DetectionDescriptorCatalog,
337373
*,

‎custom_components/eybond_local/protocol_catalogs/inverter_catalog.json‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"schema_version": 2,
3-
"catalog_version": "2026.08.5",
3+
"catalog_version": "2026.08.6",
44
"description": "Universal offline inverter catalog with reusable protocol actions, runtime surfaces, and deterministic identity evidence.",
55
"protocols": [
66
{
@@ -1307,7 +1307,7 @@
13071307
"driver_key": "modbus_smg",
13081308
"variant_key": "anenji_hhs_11kw_wifi_no_parallel",
13091309
"profile_name": "",
1310-
"register_schema_name": "modbus_smg/models/anenji_anj_11kw_48v_wifi_p.json",
1310+
"register_schema_name": "modbus_smg/models/anenji_hhs_11kw_wifi_no_parallel.json",
13111311
"tier": "partial",
13121312
"read_only": true,
13131313
"support_capture_policy": "smg_anenji_11k"
@@ -2751,9 +2751,10 @@
27512751
{
27522752
"entry_key": "deye_3ph_high_80kw",
27532753
"surface_key": "deye_3ph_high_80kw_untested",
2754-
"model_name": "Deye-Compatible Three-Phase Hybrid 80 kW (Modbus)",
2754+
"model_name": "Deye-Compatible Three-Phase Hybrid 8 kW (Modbus)",
27552755
"aliases": [
2756-
"Kevolt PD0080G-TPM-EU"
2756+
"Kevolt PD0080G-TPM-EU",
2757+
"Deye-Compatible Three-Phase Hybrid 80 kW (Modbus)"
27572758
],
27582759
"detection_supersedes_protocols": [
27592760
"modbus_smg"
@@ -2783,7 +2784,7 @@
27832784
],
27842785
"provenance": {
27852786
"sources": [
2786-
"GitHub issue #15 identifies the commercial hardware as Kevolt PD0080G-TPM-EU. Its SmartValue proxy capture repeatedly shows Modbus FC03, device type 5, protocol version 0x0104, and low-first rated-power words 0x3880/0x0001 (80000 raw), followed by the documented Deye-compatible three-phase runtime map at registers 500-683. The public protocol-document mirror corroborates register meanings, while the captured request/response pairs establish the exact wire layout."
2787+
"GitHub issue #15 identifies the commercial hardware as Kevolt PD0080G-TPM-EU 8 kW. Its SmartValue proxy capture repeatedly shows Modbus FC03, device type 5, protocol version 0x0104, and low-first rated-power words 0x3880/0x0001 (80000 raw, 0.1 W scale = 8000 W), followed by the documented Deye-compatible three-phase runtime map at registers 500-683. The public protocol-document mirror corroborates register meanings, while the captured request/response pairs establish the exact wire layout."
27872788
],
27882789
"confidence": "capture-confirmed"
27892790
}

‎custom_components/eybond_local/protocol_catalogs/register_schemas/deye_3ph_high_80kw/base.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@
153153
"key": "rated_power",
154154
"register": 20,
155155
"word_count": 2,
156-
"combine": "u32_low_first"
156+
"combine": "u32_low_first",
157+
"divisor": 10,
158+
"decimals": 1
157159
},
158160
{
159161
"key": "run_state",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"extends": "./anenji_anj_11kw_48v_wifi_p.json",
3+
"schema_key": "modbus_smg_anenji_hhs_11kw_wifi_no_parallel",
4+
"title": "Anenji HHS-11kW-WIFI Non-Parallel Register Schema",
5+
"spec_sets": {
6+
"value_overrides": [
7+
{
8+
"key": "grid_power",
9+
"register": 340,
10+
"signed": true
11+
}
12+
]
13+
}
14+
}

‎custom_components/eybond_local/runtime/hub/common.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
from ...metadata.compiled_detection_catalog import (
7979
PROBE_ACTION_MODBUS_READ,
8080
load_compiled_detection_catalog,
81+
model_names_share_catalog_identity,
8182
)
8283
from ...telemetry import TypedTelemetryFrame, fold_driver_telemetry
8384
from ...payload.modbus import ModbusSession, to_signed_16
@@ -202,7 +203,12 @@ def _inverter_identities_conflict(current: object, candidate: object) -> bool:
202203
return True
203204
cur_model = str(getattr(current, "model_name", "") or "").strip()
204205
cand_model = str(getattr(candidate, "model_name", "") or "").strip()
205-
if cur_model and cand_model and cur_model != cand_model:
206+
if (
207+
cur_model
208+
and cand_model
209+
and cur_model != cand_model
210+
and not model_names_share_catalog_identity(cur_model, cand_model)
211+
):
206212
return True
207213
return False
208214

0 commit comments

Comments
 (0)