Skip to content

Commit 0d0f929

Browse files
committed
Make inverter serial optional for hybrid Modbus setup
Issue #20's follow-up added serial auto-detection (registers 33004-33019) but the schema still required typing something into a blank field even though hybrid models immediately overwrite it once validated. CONF_INVERTER_SERIAL is now optional (default ""); if it's still blank after validation (grid/string model, or a failed detection read), the form is shown again with a clear error instead of silently creating an entry with an empty serial. The dual-protocol Modbus step needs no equivalent change: its final entry serial always comes from the cloud-resolved value, not the Modbus step's data.
1 parent 3a85660 commit 0d0f929

3 files changed

Lines changed: 104 additions & 4 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,11 @@ Home Assistant only allows one `iot_class` value in the manifest. SolisConnect i
117117
- The SolisCloud API is rate-limited by the integration. Avoid running another cloud integration against the same key if you are seeing slow or failed cloud updates.
118118
- If older debug logs captured SolisCloud credentials before redaction was added, rotate the SolisCloud key secret and clear old logs.
119119

120-
**Inverter Serial**: (Required)
120+
**Inverter Serial**: (Optional on hybrid Modbus models and SolisCloud; required otherwise)
121121

122-
- Enter your inverter's serial number. This is now **mandatory** for generating unique entity IDs and ensuring configuration stability.
123-
- On Modbus hybrid inverters, once connectivity is confirmed the integration reads the serial directly from the device and silently corrects a mistyped value (grid/string models don't expose this register, so the typed value is used as-is). On SolisCloud, the serial is optional and auto-resolved when your plant has exactly one inverter.
122+
- Used to generate unique entity IDs and ensure configuration stability — every entry needs one, but you don't always have to type it.
123+
- On Modbus **hybrid** inverters, you can leave this blank: once connectivity is confirmed, the integration reads the serial directly from the device and uses it (silently correcting a mistyped value too, if you did type one). **Grid/string** models don't expose this register, so you still need to type it there — if you leave it blank on a grid/string model, the form will ask again.
124+
- On SolisCloud, the serial is optional and auto-resolved when your plant has exactly one inverter; you only need to type it to disambiguate a plant with multiple inverters.
124125

125126
**Poll Interval**:
126127

‎custom_components/solisconnect/config_flow.py‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@
7575
# Base schema with common fields (for both TCP and Serial)
7676
BASE_CONFIG_SCHEMA = {
7777
vol.Required(CONF_CONNECTION_TYPE, default=CONN_TYPE_TCP): vol.In(CONNECTION_TYPES),
78-
vol.Required(CONF_INVERTER_SERIAL): str,
78+
# Optional: hybrid inverters have their serial read from the device (registers
79+
# 33004-33019) once connectivity is confirmed, so it doesn't need to be typed. Grid/
80+
# string models don't expose that register range, so _create_entry_from_input (and the
81+
# dual-mode Modbus step) re-check after validation and ask again if it's still blank.
82+
vol.Optional(CONF_INVERTER_SERIAL, default=""): str,
7983
vol.Required("slave", default=1): int,
8084
vol.Optional("poll_interval_fast", default=10): vol.All(int, vol.Range(min=10)),
8185
vol.Optional("poll_interval_normal", default=15): vol.All(int, vol.Range(min=15)),
@@ -538,6 +542,17 @@ async def _create_entry_from_input(self, data):
538542

539543
return self.async_show_form(step_id="config", data_schema=vol.Schema(schema_dict), errors=errors)
540544

545+
# 2b. The serial field is optional because hybrid models auto-detect it during
546+
# validation above; grid/string models don't expose that register range, so if it's
547+
# still blank at this point there's no way to identify the device — ask for it.
548+
if not (data.get(CONF_INVERTER_SERIAL) or "").strip():
549+
errors["base"] = "Could not detect the inverter serial automatically; please enter it manually."
550+
if data.get(CONF_CONNECTION_TYPE) == CONN_TYPE_TCP:
551+
schema_dict = TCP_CONFIG_SCHEMA
552+
else:
553+
schema_dict = SERIAL_CONFIG_SCHEMA
554+
return self.async_show_form(step_id="config", data_schema=self.add_suggested_values_to_schema(vol.Schema(schema_dict), data), errors=errors)
555+
541556
# 3. Check for Duplicates
542557
if CONF_INVERTER_SERIAL in data:
543558
await self.async_set_unique_id(data[CONF_INVERTER_SERIAL])

‎tests/test_config_flow.py‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,90 @@ async def fake_read_input_register(register, count):
253253
assert 33002 in registers_read # HMI-version detection is not gated by inverter type
254254

255255

256+
@pytest.mark.asyncio
257+
async def test_flow_user_blank_serial_auto_detected_from_device(hass: HomeAssistant):
258+
"""The serial field is optional: a hybrid model can be set up without typing it at all."""
259+
device_serial_registers = _encode_serial_registers("1234567890123456")
260+
261+
async def fake_read_input_register(register, count):
262+
if register == 33004:
263+
return device_serial_registers
264+
return [1]
265+
266+
with (
267+
patch("custom_components.solisconnect.modbus_controller.ModbusController.connect", return_value=True),
268+
patch(
269+
"custom_components.solisconnect.modbus_controller.ModbusController.async_read_input_register",
270+
side_effect=fake_read_input_register,
271+
),
272+
patch("custom_components.solisconnect.async_setup_entry", return_value=True),
273+
):
274+
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": config_entries.SOURCE_USER})
275+
result = await hass.config_entries.flow.async_configure(result["flow_id"], user_input={"connection_type": CONN_TYPE_TCP})
276+
277+
config_input = {
278+
"host": "1.2.3.4",
279+
"port": 502,
280+
"slave": 1,
281+
"model": "S6-EH1P",
282+
"connection": "S2_WL_ST",
283+
"has_v2": True,
284+
"has_pv": True,
285+
"has_ac_coupling": False,
286+
"has_parallel": False,
287+
"has_battery": True,
288+
"has_hv_battery": False,
289+
"has_generator": False,
290+
# inverter_serial omitted entirely
291+
}
292+
result = await hass.config_entries.flow.async_configure(result["flow_id"], user_input=config_input)
293+
294+
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
295+
assert result["data"]["inverter_serial"] == "1234567890123456"
296+
297+
298+
@pytest.mark.asyncio
299+
async def test_flow_user_blank_serial_without_detection_shows_error(hass: HomeAssistant):
300+
"""A grid model with no typed serial has no way to auto-detect one; the form must ask again."""
301+
302+
async def fake_read_input_register(register, count):
303+
if register == 33004:
304+
return None # grid models don't expose this range anyway; belt-and-braces
305+
return [1]
306+
307+
with (
308+
patch("custom_components.solisconnect.modbus_controller.ModbusController.connect", return_value=True),
309+
patch(
310+
"custom_components.solisconnect.modbus_controller.ModbusController.async_read_input_register",
311+
side_effect=fake_read_input_register,
312+
),
313+
patch("custom_components.solisconnect.async_setup_entry", return_value=True),
314+
):
315+
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": config_entries.SOURCE_USER})
316+
result = await hass.config_entries.flow.async_configure(result["flow_id"], user_input={"connection_type": CONN_TYPE_TCP})
317+
318+
config_input = {
319+
"host": "1.2.3.4",
320+
"port": 502,
321+
"slave": 1,
322+
"model": "S6-GR1P",
323+
"connection": "S2_WL_ST",
324+
"has_v2": True,
325+
"has_pv": True,
326+
"has_ac_coupling": False,
327+
"has_parallel": False,
328+
"has_battery": True,
329+
"has_hv_battery": False,
330+
"has_generator": False,
331+
# inverter_serial omitted; grid model can't auto-detect it
332+
}
333+
result = await hass.config_entries.flow.async_configure(result["flow_id"], user_input=config_input)
334+
335+
assert result["type"] == data_entry_flow.FlowResultType.FORM
336+
assert result["step_id"] == "config"
337+
assert "manually" in result["errors"]["base"]
338+
339+
256340
@pytest.mark.asyncio
257341
async def test_flow_user_connection_error(hass: HomeAssistant):
258342
"""Test user initialized flow with connection error."""

0 commit comments

Comments
 (0)