This document provides comprehensive guidelines for AI assistants (and developers) working on the Growatt Modbus Home Assistant integration.
BEFORE making ANY changes to sensors or registers:
□ Step 1: Update profile file (profiles/*.py) - Add register definition
□ Step 2: Add to GrowattData dataclass (growatt_modbus.py) - Add field
□ Step 3: Add sensor definition (sensor.py) - SENSOR_DEFINITIONS
□ Step 4: Assign device type (const.py) - SENSOR_DEVICE_MAP
□ Step 5: Add to sensor group (device_profiles.py) - BATTERY_SENSORS/GRID_SENSORS/etc
□ Step 6: Run validation script: python3 validate_sensors.py --sensor <name>
# Validate a specific sensor
python3 validate_sensors.py --sensor battery_power
# Validate all sensors
python3 validate_sensors.py
# Validate entire profile
python3 validate_sensors.py --profile sph# After making changes, verify sensor appears in all places:
grep -r "your_sensor_name" custom_components/growatt_modbus/If ANY step is skipped, the sensor WILL NOT work correctly!
See Register Update/Addition Process below for detailed instructions.
- Architecture Overview
- Register Update/Addition Process
- Profile Management
- Critical Patterns & Conventions
- Testing & Validation
- Common Issues & Solutions
custom_components/growatt_modbus/
├── profiles/ # Register maps by inverter family
│ ├── sph.py # SPH single-phase hybrid
│ ├── spf.py # SPF off-grid
│ ├── mod.py # MOD three-phase hybrid
│ ├── min.py # MIN grid-tied
│ └── wit.py # WIT commercial hybrid
├── device_profiles.py # Profile registry & display names
├── const.py # Device mappings, entity categories
├── sensor.py # Sensor entity definitions
├── number.py # Number entity definitions
├── select.py # Select entity definitions
├── coordinator.py # Data coordinator & device info
├── auto_detection.py # Automatic profile detection
└── diagnostic.py # Register scanning & diagnostics
The integration uses a multi-device structure where entities are logically grouped:
Inverter (parent device)
├── Solar Device - PV inputs, AC output, solar energy
├── Grid Device - Grid import/export, grid power flow
├── Load Device - Consumption, load power, AC output voltage
└── Battery Device - Battery storage, charge/discharge
Why this matters: When adding sensors, you MUST assign them to the correct device type or they'll appear in the wrong location in Home Assistant.
When adding or updating a register in a profile, you MUST complete ALL these steps:
Location: custom_components/growatt_modbus/profiles/<profile>.py
Add the register definition:
'input_registers': {
20: {'name': 'grid_voltage', 'scale': 0.1, 'unit': 'V', 'desc': 'AC input voltage'},
}Important considerations:
- Choose the correct register name (see Naming Conventions)
- Set correct
scalefactor - For 32-bit values, define both
_highand_lowwithpairattribute - Mark signed values with
'signed': True
Location: custom_components/growatt_modbus/growatt_modbus.py (~lines 60-180)
Add the field to the @dataclass definition:
@dataclass
class GrowattData:
"""Container for Growatt inverter data"""
# ... existing fields ...
# SPF Off-Grid AC Input (from grid/generator)
grid_voltage: float = 0.0 # V (AC input voltage)
grid_frequency: float = 0.0 # Hz (AC input frequency)- The code will try to set
data.grid_voltage = value - But
hasattr(data, 'grid_voltage')returnsFalse - Sensor conditions fail and sensor doesn't appear
Type Guidelines:
float = 0.0- For all sensor values (voltage, current, power, energy, temperature, percentages)int = 0- For status codes, counts, or control registersstr = ""- For text fields (firmware version, serial number)
Organization:
- Group related fields together with comment headers
- Place near similar sensors (battery fields together, PV fields together, etc.)
- See existing dataclass structure for examples
Location: custom_components/growatt_modbus/sensor.py
Add to SENSOR_DEFINITIONS dictionary (~line 40-700):
"grid_voltage": {
"name": "Grid Voltage",
"icon": "mdi:transmission-tower",
"device_class": SensorDeviceClass.VOLTAGE,
"state_class": SensorStateClass.MEASUREMENT,
"unit": UnitOfElectricPotential.VOLT,
"attr": "grid_voltage", # Must match register name
},Required fields:
name- Display name in Home Assistanticon- MDI icon (browse at https://materialdesignicons.com/)device_class- HA device class (VOLTAGE, POWER, ENERGY, etc.)state_class- UsuallyMEASUREMENTfor sensorsunit- Unit constant fromhomeassistant.constattr- Data attribute name (must match registernamein profile)
Optional fields:
condition- Lambda function to conditionally create sensorentity_category- Set toEntityCategory.DIAGNOSTICfor technical sensors
Location: custom_components/growatt_modbus/const.py (~line 416-488)
Add the sensor key to the appropriate device's set in SENSOR_DEVICE_MAP:
SENSOR_DEVICE_MAP = {
DEVICE_TYPE_GRID: {
'grid_power', 'grid_export_power',
'grid_voltage', # ← Add here
...
},
}Device assignment guidelines:
DEVICE_TYPE_SOLAR- PV inputs, solar production, AC output current/powerDEVICE_TYPE_GRID- Grid connection, import/export, grid voltage/frequencyDEVICE_TYPE_LOAD- Consumption, load power, AC output voltage (for SPF)DEVICE_TYPE_BATTERY- Battery storage, SOC, charge/dischargeDEVICE_TYPE_INVERTER- Status, faults, temperatures, system info
Location: custom_components/growatt_modbus/device_profiles.py (Lines 5-110)
Add the sensor key to the appropriate sensor group set:
BATTERY_SENSORS: Set[str] = {
"battery_voltage", "battery_current", "battery_soc",
"battery_temp", "battery_power",
"new_battery_sensor", # ← Add here
...
}
GRID_SENSORS: Set[str] = {
"grid_power", "grid_export_power",
"grid_voltage", # ← Or add here if it's a grid sensor
...
}Available sensor groups:
BASIC_PV_SENSORS- PV string sensors (voltage, current, power)BASIC_AC_SENSORS- AC output sensorsBATTERY_SENSORS- Battery related sensorsGRID_SENSORS- Grid import/export sensorsENERGY_SENSORS- Energy production sensorsTEMPERATURE_SENSORS- Temperature sensorsSTATUS_SENSORS- Status and diagnostic sensorsTHREE_PHASE_SENSORS- Three-phase AC sensorsSPF_OFFGRID_SENSORS- Off-grid specific sensors
Why this matters: Profiles in INVERTER_PROFILES compose these sensor groups (e.g., sensors: BASIC_PV_SENSORS | BATTERY_SENSORS). If the sensor isn't in the right group, it won't be included in any profile.
Run the validation script (REQUIRED):
python3 validate_sensors.py --sensor your_sensor_nameThis will automatically check:
- ✅ Register defined in profile
- ✅ Added to sensor.py SENSOR_DEFINITIONS
- ✅ Added to const.py SENSOR_DEVICE_MAP
- ✅ Added to profile 'sensors' set
Additional manual checks:
-
Search for similar register names to ensure consistency:
grep -r "grid_voltage" custom_components/growatt_modbus/ -
Verify no naming conflicts in the profile:
grep "'name': 'grid_voltage'" profiles/*.py
-
Check if register needs special handling in coordinator.py:
- Signed value conversion?
- Inversion (like battery power for SPF)?
- Fallback logic (like battery voltage)?
-
Update tests (if applicable)
Create a new profile when:
- Inverter uses a different register range (e.g., 0-124 vs 3000-3124)
- Register addresses overlap but have different meanings
- Significantly different capabilities (e.g., battery vs no battery)
Extend existing profile when:
- Only adding optional sensors (e.g., PV3 for 3-string models)
- Different power ratings but same register map
- Minor firmware variations
Profiles can inherit from others using Python's ** operator:
SPH_8000_10000_HU = {
'name': 'SPH/SPM 8000-10000TL-HU',
'input_registers': {
# Inherit base registers
**SPH_7000_10000['input_registers'],
# Add/override specific registers
1086: {'name': 'bms_soc', 'scale': 1, 'unit': '%'},
},
}Benefits:
- Reduces code duplication
- Makes differences explicit
- Easier to maintain
Risks:
- Changes to base profile affect all children
- Must verify overrides don't break inheritance
The integration supports two protocol families:
- Legacy Protocol - Older models, 0-124 register range
- VPP V2.01 - Newer models, 31000+ registers, DTC codes
Auto-detection logic:
- Try to read DTC code (register 30000)
- If present, use DTC-to-profile mapping
- If not, use legacy detection (PV voltage, range checks)
When adding profiles:
- Determine protocol version first
- Set
'offgrid_protocol': Truefor SPF (prevents VPP register access) - Use appropriate register ranges
Register names are critical because they control fallback behavior:
'battery_voltage' # Coordinator searches for this exact name
'battery_soc' # Falls back through: soc → battery_soc
'battery_power_low' # Falls back to: charge_power_low / discharge_power_low'battery_voltage_legacy' # Coordinator won't find "battery_voltage"
'battery_soc_vpp' # Coordinator won't find "battery_soc"
'battery_power_vpp_low' # Coordinator won't find "battery_power_low"Use case for suffixes: When multiple register ranges have the same logical sensor but only one works:
# MOD profile - VPP range doesn't respond, 3000+ range does
31200: {'name': 'battery_power_vpp_high', ...}, # Won't be found
31201: {'name': 'battery_power_vpp_low', ...}, # Won't be found
3178: {'name': 'battery_charge_power_high', ...}, # Will be found ✓
3179: {'name': 'battery_charge_power_low', ...}, # Will be found ✓Many registers are 32-bit values split across two 16-bit registers:
77: {
'name': 'battery_power_high',
'scale': 1,
'unit': '',
'pair': 78, # Points to low word
'signed': True,
'desc': 'Battery power (HIGH word)'
},
78: {
'name': 'battery_power_low',
'scale': 1,
'unit': '',
'pair': 77, # Points to high word
'combined_scale': 0.1, # Scale after combining
'combined_unit': 'W', # Unit after combining
'signed': True,
'desc': 'Battery power (LOW word)'
},Key points:
- Both registers must have
'pair'pointing to each other - The
_lowregister typically hascombined_scaleandcombined_unit - Combined value =
(high << 16) | low - Apply
signedconversion BEFORE scaling
Battery Power:
Positive = Charging
Negative = Discharging
Grid Power:
Positive = Exporting to grid
Negative = Importing from grid
SPF uses inverted convention for battery power:
Battery Power (Hardware):
Positive = Discharging ❌
Negative = Charging ❌
Solution: Use negative scale to flip:
78: {
'name': 'battery_power_low',
'combined_scale': -0.1, # Negative scale inverts sign
'signed': True,
}Why this matters: Home Assistant and energy dashboards expect standard convention. SPF is the only exception.
Some registers have different scales depending on the device:
# SPF: Battery voltage uses 0.01 scale for precision
17: {'name': 'battery_voltage', 'scale': 0.01, 'unit': 'V'},
# SPH: Battery voltage uses 0.1 scale
13: {'name': 'battery_voltage', 'scale': 0.1, 'unit': 'V'},Always verify scale by checking:
- Official Modbus documentation
- Actual register values vs expected values
- Other similar models
Before committing changes:
-
Check syntax:
python3 -m py_compile custom_components/growatt_modbus/profiles/*.py python3 -m py_compile custom_components/growatt_modbus/*.py
-
Search for register name across project:
grep -r "register_name" custom_components/ -
Verify sensor appears in all required locations:
- Profile
'input_registers'or'holding_registers' - Profile
'sensors'set sensor.pySENSOR_DEFINITIONSconst.pySENSOR_DEVICE_MAP
- Profile
-
Check for naming conflicts:
grep -E "^ [0-9]+:" profiles/sph.py | grep "register_address"
When adding a new profile:
-
Test auto-detection (if applicable):
- Add DTC code to
auto_detection.pyDTC_MAP - Add refinement logic if needed (e.g., storage range check)
- Test with diagnostic scanner
- Add DTC code to
-
Verify register readings:
- Use diagnostic service
growatt_modbus.read_register - Check raw values match expected with correct scale
- Verify 32-bit combined values
- Use diagnostic service
-
Test with actual hardware (if possible):
- Use Universal Scanner diagnostic service
- Verify all sensors appear
- Check values are reasonable
-
Document in release notes:
- Add profile to supported models list
- Note any special considerations
- Include known limitations
Symptoms: Register defined in profile, but sensor doesn't appear in HA. Logs show "condition not met".
Checklist:
- ✅ Added field to
GrowattDatadataclass ingrowatt_modbus.py? ← Most common issue! - ✅ Added to appropriate sensor group in
device_profiles.py? - ✅ Added to
sensor.pySENSOR_DEFINITIONS? - ✅ Added to
const.pySENSOR_DEVICE_MAP? - ✅
attrin sensor definition matches registernameAND dataclass field name? - ✅ Condition in sensor definition evaluates to true?
Common cause: Field missing from GrowattData dataclass → hasattr(data, 'field_name') returns False → condition fails
Symptoms: Sensor appears in wrong device (e.g., battery sensor in solar device)
Solution: Update const.py SENSOR_DEVICE_MAP to assign sensor to correct device type.
Symptoms: Sensor shows wrong value (too high, too low, negative when should be positive)
Common causes:
- Wrong scale - Check register documentation
- Missing signed flag - Add
'signed': Truefor signed registers - Incorrect paired register - Verify high/low word order
- Wrong combined_scale - Check which register has combined_scale
- SPF sign inversion - Use negative scale for battery_power
Symptoms: Sensor shows 0 or unavailable when fallback register has data
Cause: Register name includes suffix that blocks fallback
Example:
# BAD - Won't fallback
31201: {'name': 'battery_power_vpp_low', ...}, # Coordinator looks for battery_power_low
# GOOD - Will fallback
31201: {'name': 'battery_power_low', ...}, # Coordinator finds itSolution: Use standard names for registers that should participate in fallback, use suffixed names for registers that shouldn't.
Symptoms: Auto-detection picks wrong profile
Common causes:
- DTC code not in mapping - Add to
auto_detection.py - Refinement logic wrong - Check range detection (storage, 3000+)
- Detection order wrong - Storage range should be checked before PV3
Solution: Update auto_detection.py with correct logic:
# CORRECT ORDER for SPH detection:
1. Check storage range (1000-1124) → SPH HU
2. Check PV3 voltage → SPH 7-10kW
3. No PV3 → SPH 3-6kWSymptoms: Update to one profile breaks others that inherit from it
Prevention:
- Check inheritance chains before modifying base profiles
- Use overrides instead of modifying shared definitions
- Test all child profiles after base changes
Solution:
# Don't modify base
BASE_PROFILE = {
1044: {'name': 'priority_mode', 'scale': 1, ...}
}
# Override in child instead
CHILD_PROFILE = {
**BASE_PROFILE,
1044: {'name': 'priority', 'scale': 1, ...} # Override
}Symptoms: MIC 1000TL-X or other micro inverters (600W-3.3kW) detected as MIN 3000-6000TL-X or communication failures
Common causes:
- Wrong detection order - Auto-detection checked MIN (3000+ range) before MIC (0-179 range)
- Serial/RTU converter misconfiguration - Wrong timing or framing settings
- Model name not recognized - Model string doesn't match patterns
Solution 1: Verify correct profile selected
- MIC uses 0-179 register range (legacy V3.05 protocol, 2013)
- MIN uses 3000+ register range (V1.39 protocol)
- These are completely different protocols - MIN profile won't work on MIC!
Solution 2: Check serial/RTU converter settings (USR-DR164, etc.)
For Modbus RTU over serial at 9600 baud:
Required Settings:
- Baud Rate: 9600
- Data Bits: 8
- Parity: None
- Stop Bits: 1 ← NOT "CTSRTS" or "2"!
- Pack Interval: 50-100ms ← NOT 20ms!
Common Mistakes:
❌ Stop Bit = "CTSRTS" → Hardware flow control not supported
❌ Pack Interval = 20ms → Too short for inverter processing
✅ Stop Bit = "1" → Standard Modbus framing
✅ Pack Interval = 50-100ms → Safe timing for 9600 baudWhy timing matters for MIC:
- Frame transmission at 9600 baud: ~10ms (8-10 bytes)
- MIC inverter processing time: 50-100ms (legacy protocol may be slower)
- Total round-trip: 100-150ms minimum
- Pack interval too short (20ms) cuts off inverter responses
Frame timing calculation:
At 9600 baud with 8N1:
- 1 bit time: 104 μs
- 1 byte (8+1+1): 1.04 ms
- Modbus read 1 register: ~8-10 bytes → 10ms transmission
- Inverter processing: 50-100ms
- Safe interval: 50-100ms between requests
Solution 3: Manual profile selection If auto-detection fails, manually select correct profile:
- Navigate to: Settings → Devices & Services → Growatt Modbus → Configure
- Select: "MIC (0.6-3.3kW)" from dropdown
- Verify sensors: ~15-20 sensors (not 40+ like MIN)
Expected MIC sensors:
- PV1 voltage/current/power (single string only)
- AC voltage/current/power/frequency
- Energy today/total
- Inverter/IPM temperature
- Status/fault codes
- NO Grid sensors (MIC doesn't have grid monitoring)
- NO PV2/PV3 sensors (MIC is single string only)
MIC model patterns recognized:
'MIC600', 'MIC750', 'MIC1000', 'MIC1500',
'MIC2000', 'MIC2500', 'MIC3000', 'MIC3300'
→ All map to: mic_600_3300tl_xDetection order (fixed in v0.2.7):
1. Check OffGrid DTC (SPF prevention)
2. Check VPP DTC (register 30000)
3. Check model name
4. Check MIC range (0-179) ← NOW BEFORE MIN
5. Check MIN range (3000+)
6. Check SPH range (battery)
7. Check 3-phase (MOD/MID)
When preparing a release:
-
Update version numbers:
manifest.json- version fieldREADME.md- version badgesconst.py- VERSION constant (if exists)
-
Update documentation:
RELEASENOTES.md- Add new version section- Document all fixes and new features
- Include upgrade notes if needed
-
Commit with proper message:
Bump version to vX.Y.Z - Feature 1 description - Fix 1 description - Update 1 description -
Test before release:
- Verify import in Home Assistant
- Check all changed sensors work
- Test with at least one real device if possible
| File | Purpose | When to Update |
|---|---|---|
profiles/*.py |
Register definitions | Adding/updating registers |
growatt_modbus.py |
Data container (GrowattData) | REQUIRED: Adding new sensor fields |
sensor.py |
Sensor entity definitions | Adding new sensors |
const.py |
Device assignments, categories | Assigning sensors to devices |
device_profiles.py |
Profile registry, sensor groups | Adding new profiles or sensors |
auto_detection.py |
Auto-detection logic | New DTC codes, refinement logic |
coordinator.py |
Data processing | Special handling, fallback logic |
diagnostic.py |
Scanner/diagnostics | Detection improvements |
Remember: This is a multi-device integration with complex fallback logic. Changes in one area can affect others unexpectedly.
When in doubt:
- Search the codebase for similar patterns
- Check how existing registers are handled
- Test with diagnostic tools before deploying
- Document architectural decisions in commit messages
For AI Assistants:
- Follow this guide completely for every register update
- Double-check all 5 steps in the checklist
- Search for similar patterns before implementing
- Ask user to verify if uncertain about device assignments
Last updated: 2026-01-29 Integration version: 0.2.7