|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | | -import asyncio |
6 | 5 | from typing import Any |
7 | 6 |
|
8 | 7 | from ..metadata.compiled_detection_catalog import load_compiled_detection_catalog |
|
18 | 17 | async_capture_modbus_snapshot, |
19 | 18 | ) |
20 | 19 | from .read_result import DriverReadMode, DriverReadResult |
| 20 | +from .support_diagnostics import capture_support_reads |
21 | 21 |
|
22 | 22 |
|
23 | 23 | # SRNE Modbus V2.07, P01 DC Data Area (addresses in that table are hexadecimal). |
24 | | -# Support-only diagnosis of a rejected 0x0100/18 read; never a runtime read plan. |
| 24 | +# Documented short reads after an explicit rejection of the 0x0100/18 block. |
25 | 25 | # In particular, do not cross the optional/new fields or reserved 0x010D again. |
26 | 26 | _DC_DIAGNOSTIC_RANGES = ( |
27 | 27 | (0x0100, 3, "battery"), |
@@ -87,17 +87,28 @@ async def async_probe(self, transport, target: ProbeTarget) -> DetectedInverter |
87 | 87 | if not _looks_like_srne_product_info(product_info): |
88 | 88 | return None |
89 | 89 |
|
90 | | - surface = load_compiled_detection_catalog().surfaces["srne_modbus_read_only"] |
| 90 | + catalog = load_compiled_detection_catalog() |
| 91 | + evidence = { |
| 92 | + "identity.product_info": product_info, |
| 93 | + "protocol.protocol_id": "SRNE_MODBUS", |
| 94 | + } |
| 95 | + resolution = catalog.resolve_family(protocol_key=self.key, evidence=evidence) |
| 96 | + if not resolution.surface_key: |
| 97 | + return None |
| 98 | + surface = catalog.surfaces[resolution.surface_key] |
91 | 99 | details = { |
92 | 100 | "product_info": product_info, |
93 | 101 | "protocol_id": "SRNE_MODBUS", |
94 | 102 | "catalog_detection": { |
95 | | - "resolution": "exact", |
| 103 | + "resolution": resolution.resolution, |
96 | 104 | "surface_key": surface.key, |
97 | | - "evidence": { |
98 | | - "identity.product_info": product_info, |
99 | | - "protocol.protocol_id": "SRNE_MODBUS", |
100 | | - }, |
| 105 | + "confidence": resolution.confidence, |
| 106 | + "candidate_keys": list(resolution.candidate_keys), |
| 107 | + "catalog_version": resolution.catalog_version, |
| 108 | + "descriptor_revisions": list(resolution.descriptor_revisions), |
| 109 | + "evidence_fingerprint": resolution.evidence_fingerprint, |
| 110 | + "evidence": evidence, |
| 111 | + "decision_path": list(resolution.decision_path), |
101 | 112 | }, |
102 | 113 | } |
103 | 114 | return DetectedInverter( |
@@ -125,7 +136,14 @@ async def async_read_values( |
125 | 136 | inverter.register_schema_name or self.register_schema_name |
126 | 137 | ) |
127 | 138 | session = self._session(transport, inverter.probe_target) |
128 | | - values = await read_spec_set_values(session, schema, ascii_style="printable") |
| 139 | + values = await read_spec_set_values( |
| 140 | + session, |
| 141 | + schema, |
| 142 | + ascii_style="printable", |
| 143 | + illegal_address_fallbacks={ |
| 144 | + (3, 0x0100, 18): tuple((start, count) for start, count, _ in _DC_DIAGNOSTIC_RANGES) |
| 145 | + }, |
| 146 | + ) |
129 | 147 | return DriverReadResult(values=values, mode=DriverReadMode.FULL) |
130 | 148 |
|
131 | 149 | async def async_write_capability( |
@@ -216,15 +234,23 @@ def local_register_read_plans( |
216 | 234 | schema = load_register_schema( |
217 | 235 | inverter.register_schema_name or self.register_schema_name |
218 | 236 | ) |
219 | | - return tuple( |
220 | | - LocalRegisterReadPlan.for_target( |
221 | | - inverter.probe_target, |
222 | | - function=3, |
223 | | - start=block.start, |
224 | | - count=block.count, |
| 237 | + # Bounded background evidence uses the same documented DC groups, not |
| 238 | + # the unsupported parent or zero-filled holes. Ordinary polling still |
| 239 | + # prefers the compact parent when the device accepts it. |
| 240 | + plans = [] |
| 241 | + for block in schema.blocks: |
| 242 | + ranges = ( |
| 243 | + tuple((start, count) for start, count, _ in _DC_DIAGNOSTIC_RANGES) |
| 244 | + if (block.start, block.count) == (0x0100, 18) |
| 245 | + else ((block.start, block.count),) |
225 | 246 | ) |
226 | | - for block in schema.blocks |
227 | | - ) |
| 247 | + plans.extend( |
| 248 | + LocalRegisterReadPlan.for_target( |
| 249 | + inverter.probe_target, function=3, start=start, count=count |
| 250 | + ) |
| 251 | + for start, count in ranges |
| 252 | + ) |
| 253 | + return tuple(plans) |
228 | 254 |
|
229 | 255 | async def async_capture_local_register_snapshot( |
230 | 256 | self, transport, inverter: DetectedInverter, *, collector_pn: str |
@@ -257,45 +283,14 @@ async def _capture_dc_subranges(session: ModbusSession) -> dict[str, Any]: |
257 | 283 | phase probing, capability writes or mutation of the runtime schema occurs. |
258 | 284 | """ |
259 | 285 |
|
260 | | - result: dict[str, Any] = { |
261 | | - "source": "SRNE Modbus V2.07, P01 DC Data Area", |
262 | | - "purpose": "support_only_rejected_dc_block", |
263 | | - "trigger_range": {"start": 0x0100, "count": 18}, |
264 | | - "time_budget_seconds": _DC_DIAGNOSTIC_TIMEOUT, |
265 | | - "planned_ranges": [ |
266 | | - {"start": start, "count": count, "group": group} |
267 | | - for start, count, group in _DC_DIAGNOSTIC_RANGES |
268 | | - ], |
269 | | - "status": "completed", |
270 | | - "captured_ranges": [], |
271 | | - "range_failures": [], |
272 | | - } |
273 | | - loop = asyncio.get_running_loop() |
274 | | - deadline = loop.time() + _DC_DIAGNOSTIC_TIMEOUT |
275 | | - for start, count, _group in _DC_DIAGNOSTIC_RANGES: |
276 | | - if loop.time() >= deadline: |
277 | | - result["status"] = "budget_exhausted" |
278 | | - break |
279 | | - timeout = asyncio.timeout_at(deadline) |
280 | | - try: |
281 | | - async with timeout: |
282 | | - words = await session.read_holding(start, count) |
283 | | - except Exception as exc: |
284 | | - expired = timeout.expired() |
285 | | - result["range_failures"].append( |
286 | | - { |
287 | | - "start": start, |
288 | | - "count": count, |
289 | | - "error": "diagnostic_budget_exhausted" if expired else str(exc), |
290 | | - } |
291 | | - ) |
292 | | - if _is_address_rejection(exc): |
293 | | - continue |
294 | | - result["status"] = "budget_exhausted" if expired else "stopped_on_error" |
295 | | - break |
296 | | - result["captured_ranges"].append( |
297 | | - {"start": start, "count": count, "words": list(words)} |
298 | | - ) |
| 286 | + result = await capture_support_reads( |
| 287 | + session, |
| 288 | + _DC_DIAGNOSTIC_RANGES, |
| 289 | + timeout_seconds=_DC_DIAGNOSTIC_TIMEOUT, |
| 290 | + source="SRNE Modbus V2.07, P01 DC Data Area", |
| 291 | + purpose="support_only_rejected_dc_block", |
| 292 | + ) |
| 293 | + result["trigger_range"] = {"start": 0x0100, "count": 18} |
299 | 294 | return result |
300 | 295 |
|
301 | 296 |
|
|
0 commit comments