Skip to content

Commit 5a96034

Browse files
committed
Improve register dump diagnostic to track ALL register attempts
Enhanced the diagnostic.py register scanning logic to provide complete visibility: Changes: - Modified _read_registers_chunked() to return status for ALL registers attempted, not just successful non-zero values - Now tracks three states: 'success', 'error' (Modbus errors), and 'exception' (Python exceptions) - Added detailed Modbus error code mapping (Illegal Data Address, etc.) - Updated CSV output to include ALL sequential registers in each range - Added "Status/Comment" column to CSV showing: * "Read OK" for successful reads with non-zero values * "Read OK (zero value)" for successful reads that returned 0 * "Modbus Error: [error type]" for Modbus protocol errors (e.g., illegal address) * Exception details for connection/timeout errors Benefits: - CSV now shows complete sequential register map with gaps clearly identified - Can distinguish between registers that returned 0 vs registers that don't exist - Modbus errors (like "Illegal Data Address") are logged so users know which registers are invalid - Better diagnostics for troubleshooting register mapping issues This addresses the issue where the CSV only showed registers with values, making it unclear if missing entries were due to zero values, Modbus errors, or invalid register addresses.
1 parent b9d443e commit 5a96034

1 file changed

Lines changed: 132 additions & 60 deletions

File tree

custom_components/growatt_modbus/diagnostic.py

Lines changed: 132 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -103,43 +103,82 @@ async def export_register_dump(call: ServiceCall) -> None:
103103
)
104104

105105

106-
def _read_registers_chunked(client, start: int, count: int, slave_id: int, chunk_size: int = 50) -> Dict[int, int]:
106+
def _read_registers_chunked(client, start: int, count: int, slave_id: int, chunk_size: int = 50) -> Dict[int, Dict[str, Any]]:
107107
"""
108108
Read registers in chunks to avoid timeouts.
109-
110-
Returns dict mapping register address to value (only non-zero values).
109+
110+
Returns dict mapping register address to: {
111+
'value': int or None,
112+
'status': 'success'|'error'|'exception',
113+
'error': str (error description if status != 'success')
114+
}
111115
"""
112116
register_data = {}
113-
117+
114118
for chunk_start in range(0, count, chunk_size):
115119
chunk_count = min(chunk_size, count - chunk_start)
116120
chunk_address = start + chunk_start
117-
121+
118122
try:
119123
response = client.read_input_registers(
120124
address=chunk_address,
121125
count=chunk_count,
122126
device_id=slave_id
123127
)
124-
128+
125129
if not response.isError():
130+
# Store ALL values, including zeros
126131
for i, value in enumerate(response.registers):
127-
if value > 0: # Only store non-zero values
128-
register_data[chunk_address + i] = value
129-
_LOGGER.debug(f"Read chunk {chunk_address}-{chunk_address+chunk_count-1}: {len([v for v in response.registers if v > 0])} non-zero")
132+
register_data[chunk_address + i] = {
133+
'value': value,
134+
'status': 'success',
135+
'error': None
136+
}
137+
_LOGGER.debug(f"Read chunk {chunk_address}-{chunk_address+chunk_count-1}: {chunk_count} registers")
130138
else:
131-
_LOGGER.debug(f"Chunk {chunk_address}-{chunk_address+chunk_count-1} returned error")
132-
139+
# Store error for each register in the chunk
140+
error_msg = str(response)
141+
# Try to extract specific error type
142+
if hasattr(response, 'exception_code'):
143+
error_code = response.exception_code
144+
error_names = {
145+
1: "Illegal Function",
146+
2: "Illegal Data Address",
147+
3: "Illegal Data Value",
148+
4: "Slave Device Failure",
149+
5: "Acknowledge",
150+
6: "Slave Device Busy",
151+
10: "Gateway Path Unavailable",
152+
11: "Gateway Target Failed to Respond"
153+
}
154+
error_msg = error_names.get(error_code, f"Error Code {error_code}")
155+
156+
for i in range(chunk_count):
157+
register_data[chunk_address + i] = {
158+
'value': None,
159+
'status': 'error',
160+
'error': error_msg
161+
}
162+
_LOGGER.debug(f"Chunk {chunk_address}-{chunk_address+chunk_count-1} returned error: {error_msg}")
163+
133164
except Exception as e:
165+
# Store exception for each register in the chunk
166+
error_msg = f"Exception: {type(e).__name__}: {str(e)}"
167+
for i in range(chunk_count):
168+
register_data[chunk_address + i] = {
169+
'value': None,
170+
'status': 'exception',
171+
'error': error_msg
172+
}
134173
_LOGGER.debug(f"Chunk {chunk_address} exception: {e}")
135-
174+
136175
return register_data
137176

138177

139-
def _detect_inverter_model(register_data: Dict[int, int]) -> Dict[str, str]:
178+
def _detect_inverter_model(register_data: Dict[int, Dict[str, Any]]) -> Dict[str, str]:
140179
"""
141180
Analyze register responses to detect inverter model.
142-
181+
143182
Returns dict with: model, confidence, profile_key, register_map, reasoning
144183
"""
145184
detection = {
@@ -149,16 +188,16 @@ def _detect_inverter_model(register_data: Dict[int, int]) -> Dict[str, str]:
149188
"register_map": "UNKNOWN",
150189
"reasoning": [],
151190
}
152-
153-
# Helper to check if register exists
191+
192+
# Helper to check if register exists with valid data
154193
def has_reg(addr):
155-
return addr in register_data
194+
return addr in register_data and register_data[addr]['status'] == 'success' and register_data[addr]['value'] is not None and register_data[addr]['value'] > 0
156195

157-
# Check register ranges
158-
has_0_124 = any(0 <= r <= 124 for r in register_data.keys())
159-
has_1000_1124 = any(1000 <= r <= 1124 for r in register_data.keys())
160-
has_3000_3124 = any(3000 <= r <= 3124 for r in register_data.keys())
161-
has_3125_3249 = any(3125 <= r <= 3249 for r in register_data.keys())
196+
# Check register ranges (only successful reads with non-zero values)
197+
has_0_124 = any(0 <= r <= 124 and register_data[r]['status'] == 'success' and register_data[r]['value'] > 0 for r in register_data.keys())
198+
has_1000_1124 = any(1000 <= r <= 1124 and register_data[r]['status'] == 'success' and register_data[r]['value'] > 0 for r in register_data.keys())
199+
has_3000_3124 = any(3000 <= r <= 3124 and register_data[r]['status'] == 'success' and register_data[r]['value'] > 0 for r in register_data.keys())
200+
has_3125_3249 = any(3125 <= r <= 3249 and register_data[r]['status'] == 'success' and register_data[r]['value'] > 0 for r in register_data.keys())
162201

163202
# Key register checks
164203
has_pv1_at_3 = has_reg(3) # PV1 voltage in 0-124 range
@@ -301,20 +340,22 @@ def _export_registers_to_csv(hass, host: str, port: int, slave_id: int) -> dict:
301340
# Scan ALL ranges
302341
all_register_data = {}
303342
range_responses = {}
304-
343+
305344
for range_config in UNIVERSAL_SCAN_RANGES:
306345
range_name = range_config["name"]
307346
start = range_config["start"]
308347
count = range_config["count"]
309-
348+
310349
_LOGGER.info(f"Scanning {range_name}...")
311-
350+
312351
registers = _read_registers_chunked(client, start, count, slave_id, chunk_size=50)
313-
352+
314353
if registers:
315354
all_register_data.update(registers)
316-
range_responses[range_name] = len(registers)
317-
_LOGGER.info(f"{range_name}: {len(registers)} non-zero registers")
355+
# Count successful non-zero reads for range summary
356+
successful_count = sum(1 for r in registers.values() if r['status'] == 'success' and r['value'] > 0)
357+
range_responses[range_name] = successful_count
358+
_LOGGER.info(f"{range_name}: {successful_count} non-zero registers out of {len(registers)} attempted")
318359
else:
319360
range_responses[range_name] = 0
320361
_LOGGER.info(f"{range_name}: No response")
@@ -369,51 +410,82 @@ def _export_registers_to_csv(hass, host: str, port: int, slave_id: int) -> dict:
369410
"×0.1",
370411
"×0.01",
371412
"Signed",
372-
"32-bit Combined (with next reg)"
413+
"32-bit Combined (with next reg)",
414+
"Status/Comment"
373415
])
374416

375417
# Write all registers sorted by address
376418
total = 0
377-
non_zero = len(all_register_data)
378-
419+
non_zero = 0
420+
379421
# Group by ranges for organized output
380422
for range_config in UNIVERSAL_SCAN_RANGES:
381423
range_name = range_config["name"]
382424
start = range_config["start"]
383425
end = start + range_config["count"]
384-
426+
427+
# Get all registers in this range
385428
range_registers = {k: v for k, v in all_register_data.items() if start <= k < end}
386-
429+
387430
if range_registers:
388431
writer.writerow([])
389432
writer.writerow([f"--- {range_name} ---"])
390-
391-
for reg_addr in sorted(range_registers.keys()):
392-
value = range_registers[reg_addr]
393-
total += 1
394-
395-
# Calculate interpretations
396-
scaled_01 = value * 0.1
397-
scaled_001 = value * 0.01
398-
signed = value - 65536 if value > 32767 else value
399-
400-
# Try to combine with next register for 32-bit values
401-
combined_32bit = ""
402-
if reg_addr + 1 in all_register_data:
403-
next_val = all_register_data[reg_addr + 1]
404-
combined = (value << 16) | next_val
405-
if 0 < combined < 10000000:
406-
combined_32bit = f"{combined} (×0.1={combined*0.1:.1f})"
407-
408-
writer.writerow([
409-
reg_addr,
410-
f"0x{reg_addr:04X}",
411-
value,
412-
f"{scaled_01:.1f}",
413-
f"{scaled_001:.2f}",
414-
signed,
415-
combined_32bit
416-
])
433+
434+
# Write ALL registers in sequential order
435+
for reg_addr in range(start, end):
436+
if reg_addr in range_registers:
437+
reg_info = range_registers[reg_addr]
438+
value = reg_info['value']
439+
status = reg_info['status']
440+
error = reg_info['error']
441+
442+
total += 1
443+
444+
# Build status/comment field
445+
if status == 'success':
446+
if value == 0:
447+
status_comment = "Read OK (zero value)"
448+
else:
449+
status_comment = "Read OK"
450+
non_zero += 1
451+
elif status == 'error':
452+
status_comment = f"Modbus Error: {error}"
453+
value = "" # Clear value field for errors
454+
else: # exception
455+
status_comment = error
456+
value = "" # Clear value field for exceptions
457+
458+
# Calculate interpretations only for successful reads
459+
if status == 'success' and value is not None:
460+
scaled_01 = value * 0.1
461+
scaled_001 = value * 0.01
462+
signed = value - 65536 if value > 32767 else value
463+
464+
# Try to combine with next register for 32-bit values
465+
combined_32bit = ""
466+
if reg_addr + 1 in all_register_data:
467+
next_info = all_register_data[reg_addr + 1]
468+
if next_info['status'] == 'success' and next_info['value'] is not None:
469+
next_val = next_info['value']
470+
combined = (value << 16) | next_val
471+
if 0 < combined < 10000000:
472+
combined_32bit = f"{combined} (×0.1={combined*0.1:.1f})"
473+
else:
474+
scaled_01 = ""
475+
scaled_001 = ""
476+
signed = ""
477+
combined_32bit = ""
478+
479+
writer.writerow([
480+
reg_addr,
481+
f"0x{reg_addr:04X}",
482+
value,
483+
f"{scaled_01:.1f}" if scaled_01 != "" else "",
484+
f"{scaled_001:.2f}" if scaled_001 != "" else "",
485+
signed,
486+
combined_32bit,
487+
status_comment
488+
])
417489

418490
result["success"] = True
419491
result["filename"] = filename

0 commit comments

Comments
 (0)