1+ """
2+ Auto-Detection System for Growatt Inverters
3+
4+ This module implements automatic inverter type detection similar to the
5+ solax-modbus plugin's async_determineInverterType function.
6+
7+ It reads the inverter's serial number and model information to automatically
8+ select the correct profile.
9+ """
10+
11+ import logging
12+ from typing import Optional , Tuple
13+
14+ from homeassistant .core import HomeAssistant
15+
16+ from .device_profiles import INVERTER_PROFILES , get_profile
17+ from .growatt_modbus import GrowattModbus
18+
19+ _LOGGER = logging .getLogger (__name__ )
20+
21+
22+ def detect_profile_from_model_name (model_name : str ) -> Optional [str ]:
23+ """
24+ Match a model name string to a profile key.
25+
26+ Args:
27+ model_name: Model name read from inverter (e.g., "MIN 10000TL-X")
28+
29+ Returns:
30+ Profile key or None if no match
31+ """
32+ if not model_name :
33+ return None
34+
35+ # Normalize model name for comparison
36+ model_upper = model_name .upper ().replace ("-" , "" ).replace (" " , "" )
37+
38+ # Model name patterns to profile mappings
39+ patterns = {
40+ # MIN series
41+ 'MIN3000' : 'min_3000_6000_tl_x' ,
42+ 'MIN4000' : 'min_3000_6000_tl_x' ,
43+ 'MIN5000' : 'min_3000_6000_tl_x' ,
44+ 'MIN6000' : 'min_3000_6000_tl_x' ,
45+ 'MIN7000' : 'min_7000_10000_tl_x' ,
46+ 'MIN8000' : 'min_7000_10000_tl_x' ,
47+ 'MIN9000' : 'min_7000_10000_tl_x' ,
48+ 'MIN10000' : 'min_7000_10000_tl_x' ,
49+
50+ # TL-XH series
51+ 'TLXH3000' : 'tl_xh_3000_10000' ,
52+ 'TLXH5000' : 'tl_xh_3000_10000' ,
53+ 'TLXH6000' : 'tl_xh_3000_10000' ,
54+ 'TLXH8000' : 'tl_xh_3000_10000' ,
55+ 'TLXH10000' : 'tl_xh_3000_10000' ,
56+ 'TLXHUS' : 'tl_xh_us_3000_10000' ,
57+
58+ # MID series
59+ 'MID15000' : 'mid_15000_25000tl3_x' ,
60+ 'MID17000' : 'mid_15000_25000tl3_x' ,
61+ 'MID20000' : 'mid_15000_25000tl3_x' ,
62+ 'MID22000' : 'mid_15000_25000tl3_x' ,
63+ 'MID25000' : 'mid_15000_25000tl3_x' ,
64+
65+ # MAC series
66+ 'MAC20000' : 'mac_20000_40000tl3_x' ,
67+ 'MAC25000' : 'mac_20000_40000tl3_x' ,
68+ 'MAC30000' : 'mac_20000_40000tl3_x' ,
69+ 'MAC36000' : 'mac_20000_40000tl3_x' ,
70+ 'MAC40000' : 'mac_20000_40000tl3_x' ,
71+
72+ # MAX series
73+ 'MAX50' : 'max_50000_125000tl3_x' ,
74+ 'MAX60' : 'max_50000_125000tl3_x' ,
75+ 'MAX75' : 'max_50000_125000tl3_x' ,
76+ 'MAX100' : 'max_50000_125000tl3_x' ,
77+ 'MAX110' : 'max_50000_125000tl3_x' ,
78+ 'MAX125' : 'max_50000_125000tl3_x' ,
79+ 'MAX1500V' : 'max_1500v_series' ,
80+ 'MAXXLV' : 'max_x_lv_series' ,
81+
82+ # SPH series
83+ 'SPH3000' : 'sph_3000_10000' ,
84+ 'SPH3600' : 'sph_3000_10000' ,
85+ 'SPH4000' : 'sph_3000_10000' ,
86+ 'SPH5000' : 'sph_3000_10000' ,
87+ 'SPH6000' : 'sph_3000_10000' ,
88+ 'SPH8000' : 'sph_3000_10000' ,
89+ 'SPH10000' : 'sph_3000_10000' ,
90+
91+ # MOD series
92+ 'MOD6000' : 'mod_6000_15000tl3_xh' ,
93+ 'MOD8000' : 'mod_6000_15000tl3_xh' ,
94+ 'MOD10000' : 'mod_6000_15000tl3_xh' ,
95+ 'MOD12000' : 'mod_6000_15000tl3_xh' ,
96+ 'MOD15000' : 'mod_6000_15000tl3_xh' ,
97+
98+ # MIX series
99+ 'MIX' : 'mix_series' ,
100+
101+ # SPA series
102+ 'SPA' : 'spa_series' ,
103+
104+ # WIT series
105+ 'WIT' : 'wit_tl3_series' ,
106+ }
107+
108+ # Try to find a match
109+ for pattern , profile_key in patterns .items ():
110+ if pattern in model_upper :
111+ _LOGGER .info (f"Matched model '{ model_name } ' to profile '{ profile_key } '" )
112+ return profile_key
113+
114+ _LOGGER .warning (f"No profile match found for model name: { model_name } " )
115+ return None
116+
117+
118+ async def async_read_serial_number (
119+ hass : HomeAssistant ,
120+ client : GrowattModbus ,
121+ device_id : int = 1
122+ ) -> Optional [str ]:
123+ """
124+ Read inverter serial number from holding registers.
125+
126+ Args:
127+ hass: HomeAssistant instance
128+ client: GrowattModbus client
129+ device_id: Modbus device ID (default 1)
130+
131+ Returns:
132+ Serial number string or None
133+ """
134+ try :
135+ # Read 10 registers starting at address 23
136+ result = await hass .async_add_executor_job (
137+ client .client .read_holding_registers ,
138+ 23 , 10
139+ )
140+
141+ if result .isError ():
142+ _LOGGER .debug (f"Error reading serial number: { result } " )
143+ return None
144+
145+ # Convert registers to string
146+ serial_bytes = []
147+ for register in result .registers :
148+ high_byte = (register >> 8 ) & 0xFF
149+ low_byte = register & 0xFF
150+ serial_bytes .extend ([high_byte , low_byte ])
151+
152+ # Convert bytes to string and strip null characters
153+ serial_number = bytes (serial_bytes ).decode ('ascii' , errors = 'ignore' ).strip ('\x00 ' ).strip ()
154+
155+ if serial_number :
156+ _LOGGER .info (f"Read serial number: { serial_number } " )
157+ return serial_number
158+
159+ return None
160+
161+ except Exception as e :
162+ _LOGGER .debug (f"Exception reading serial number: { str (e )} " )
163+ return None
164+
165+
166+ async def async_read_model_name (
167+ hass : HomeAssistant ,
168+ client : GrowattModbus ,
169+ device_id : int = 1
170+ ) -> Optional [str ]:
171+ """
172+ Read inverter model name from holding registers.
173+
174+ Args:
175+ hass: HomeAssistant instance
176+ client: GrowattModbus client
177+ device_id: Modbus device ID (default 1)
178+
179+ Returns:
180+ Model name string or None
181+ """
182+ try :
183+ # Read 5 registers starting at address 0
184+ result = await hass .async_add_executor_job (
185+ client .client .read_holding_registers ,
186+ 0 , 5
187+ )
188+
189+ if result .isError ():
190+ _LOGGER .debug (f"Error reading model name: { result } " )
191+ return None
192+
193+ # Convert registers to string
194+ model_bytes = []
195+ for register in result .registers :
196+ high_byte = (register >> 8 ) & 0xFF
197+ low_byte = register & 0xFF
198+ model_bytes .extend ([high_byte , low_byte ])
199+
200+ # Convert bytes to string and strip null characters
201+ model_name = bytes (model_bytes ).decode ('ascii' , errors = 'ignore' ).strip ('\x00 ' ).strip ()
202+
203+ if model_name :
204+ _LOGGER .info (f"Read model name: { model_name } " )
205+ return model_name
206+
207+ return None
208+
209+ except Exception as e :
210+ _LOGGER .debug (f"Exception reading model name: { str (e )} " )
211+ return None
212+
213+
214+ async def async_detect_inverter_series (
215+ hass : HomeAssistant ,
216+ client : GrowattModbus ,
217+ device_id : int = 1
218+ ) -> Optional [str ]:
219+ """
220+ Detect inverter series by probing different register ranges.
221+
222+ Args:
223+ hass: HomeAssistant instance
224+ client: GrowattModbus client
225+ device_id: Modbus device ID
226+
227+ Returns:
228+ Profile key or None
229+ """
230+ try :
231+ # Test for battery registers at 3169 (SPH/TL-XH/MOD specific)
232+ result = await hass .async_add_executor_job (
233+ client .client .read_input_registers ,
234+ 3169 , 1 ,
235+ )
236+ if not result .isError () and result .registers [0 ] > 0 :
237+ _LOGGER .info ("Detected battery voltage register - hybrid inverter" )
238+
239+ # Check for 3-phase at register 42 (MOD uses R/S/T phases)
240+ phase_test = await hass .async_add_executor_job (
241+ client .client .read_input_registers ,
242+ 42 , 1
243+ )
244+ if not phase_test .isError ():
245+ _LOGGER .info ("Detected 3-phase hybrid - MOD series" )
246+ return 'mod_6000_15000tl3_xh'
247+ else :
248+ _LOGGER .info ("Detected single-phase hybrid - SPH/TL-XH series" )
249+ return 'sph_3000_10000' # Default to SPH
250+
251+ # Test for 3-phase at register 38 (MID/MAC/MAX)
252+ result = await hass .async_add_executor_job (
253+ client .client .read_input_registers ,
254+ 38 , 1
255+ )
256+ if not result .isError ():
257+ # Check register 42 for second phase
258+ phase2 = await hass .async_add_executor_job (
259+ client .client .read_input_registers ,
260+ 42 , 1
261+ )
262+ if not phase2 .isError ():
263+ _LOGGER .info ("Detected 3-phase grid-tied inverter - MID/MAX series" )
264+ return 'mid_15000_25000tl3_x' # Default to MID
265+
266+ # Test for PV3 at register 11 (MIN 7-10k has 3 strings)
267+ result = await hass .async_add_executor_job (
268+ client .client .read_input_registers ,
269+ 11 , 1
270+ )
271+ if not result .isError () and result .registers [0 ] > 0 :
272+ _LOGGER .info ("Detected 3 PV strings - MIN 7000-10000TL-X" )
273+ return 'min_7000_10000_tl_x'
274+
275+ # Default to MIN 3-6k if nothing else detected
276+ _LOGGER .info ("Detected single-phase with 2 PV strings - MIN 3000-6000TL-X" )
277+ return 'min_3000_6000_tl_x'
278+
279+ except Exception as e :
280+ _LOGGER .error (f"Exception detecting inverter series: { str (e )} " )
281+ return None
282+
283+
284+ async def async_determine_inverter_type (
285+ hass : HomeAssistant ,
286+ client : GrowattModbus ,
287+ device_id : int = 1
288+ ) -> Tuple [Optional [str ], Optional [dict ]]:
289+ """
290+ Automatically determine the inverter type and return appropriate profile.
291+
292+ Process:
293+ 1. Read model name from holding registers
294+ 2. Attempt to match model name to known profiles
295+ 3. If no match, detect series by probing registers
296+ 4. Return the appropriate profile
297+
298+ Args:
299+ hass: HomeAssistant instance
300+ client: GrowattModbus client
301+ device_id: Modbus device ID (default 1)
302+
303+ Returns:
304+ Tuple of (profile_key, profile_dict) or (None, None) if detection fails
305+ """
306+ _LOGGER .info ("Starting automatic inverter type detection" )
307+
308+ # Step 1: Try to read model name
309+ model_name = await async_read_model_name (hass , client , device_id )
310+
311+ if model_name :
312+ # Step 2: Try to match model name to profile
313+ profile_key = detect_profile_from_model_name (model_name )
314+
315+ if profile_key :
316+ profile = get_profile (profile_key )
317+ if profile :
318+ _LOGGER .info (f"✓ Auto-detected from model name: { profile ['name' ]} " )
319+ return profile_key , profile
320+
321+ # Step 3: Model name didn't work, try series detection
322+ _LOGGER .info ("Model name detection failed, trying register-based detection..." )
323+ profile_key = await async_detect_inverter_series (hass , client , device_id )
324+
325+ if profile_key :
326+ profile = get_profile (profile_key )
327+ if profile :
328+ _LOGGER .warning (
329+ f"⚠ Auto-detected by probing registers: { profile ['name' ]} . "
330+ "Consider manually verifying the exact model for best accuracy."
331+ )
332+ return profile_key , profile
333+
334+ # Step 4: Everything failed
335+ _LOGGER .error ("❌ Could not auto-detect inverter type" )
336+ return None , None
0 commit comments