Skip to content

Commit 1315534

Browse files
Bl00d-B0bclaude
andcommitted
fix(solax): correct firmware version display and simplify per-inverter-type
The MIC GEN2 software version was rendered as "DSP v2.136 ARM v2.136": a hardcoded "v2." prefix glued onto a ÷100-encoded register value (136 = 1.36). This fixes the display and adds the ARM bootloader suffix, matching SolaX Cloud. Firmware version formatting is now split into three focused functions by inverter type (per review feedback), instead of one branchy function: - value_function_software_version_mic MIC (all gens): firmware_dsp/firmware_arm are the full ÷100 value; GEN2 adds the 0x354 ARM bootloader (÷100) as a suffix. - value_function_software_version_hybrid_legacy AC/HYBRID protocol < 100: version rendered directly as major.minor from the split registers (no multiply-then-split round-trip). - value_function_software_version_hybrid_full AC/HYBRID protocol >= 100: full ÷100 value from combined 0x7B/0x7C. A thin value_function_software_version dispatches AC/HYBRID to full/legacy by modbus protocol version (0x82). GEN2/GEN3 keep their existing legacy display functions. New internal sensor firmware_arm_boot (0x354) added for MIC GEN2. Key naming unified: firmware_dsp/firmware_arm = full ÷100 value everywhere; split parts renamed firmware_dsp_minor/firmware_arm_minor. Live-verified against real devices (SolaX Cloud vs HA): X3-MIC/PRO-G2 : "DSP v2.136 ARM v2.136" -> "DSP 1.36 ARM 1.36-1.00" X3-Hybrid-G4 : "DSP 1.60 ARM 1.58" -> "DSP 1.60 ARM 1.58-1.15" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 350266f commit 1315534

1 file changed

Lines changed: 82 additions & 80 deletions

File tree

custom_components/solax_modbus/plugin_solax.py

Lines changed: 82 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -267,12 +267,12 @@ async def async_read_inverter_firmware_info(hub: Any) -> int:
267267
inverter_data = await hub.async_read_holding_registers(unit=hub._modbus_addr, address=0x7D, count=8)
268268
if inverter_data is not None and not inverter_data.isError():
269269
registers = inverter_data.registers
270-
data["firmware_dsp"] = int(registers[0])
270+
data["firmware_dsp_minor"] = int(registers[0])
271271
data["firmware_DSP_hardware_version"] = int(registers[1])
272272
data["firmware_dsp_major"] = int(registers[2])
273273
data["firmware_arm_major"] = int(registers[3])
274274
version = int(registers[5])
275-
data["firmware_arm"] = int(registers[6])
275+
data["firmware_arm_minor"] = int(registers[6])
276276
data["bootloader_version"] = int(registers[7])
277277
except Exception:
278278
_LOGGER.debug(f"{hub.name}: attempt to read inverter firmware info failed data: {inverter_data}", exc_info=True)
@@ -284,8 +284,8 @@ async def async_read_inverter_firmware_info(hub: Any) -> int:
284284
else:
285285
hub.modbus_protocol_version = None
286286
data.pop("modbus_protocol_version", None)
287-
data.pop("firmware_version_dsp", None)
288-
data.pop("firmware_version_arm", None)
287+
data.pop("firmware_dsp", None)
288+
data.pop("firmware_arm", None)
289289
_LOGGER.debug(f"{hub.name}: Modbus protocol document version unavailable")
290290
return 0
291291

@@ -294,13 +294,13 @@ async def async_read_inverter_firmware_info(hub: Any) -> int:
294294
try:
295295
full_version_data = await hub.async_read_holding_registers(unit=hub._modbus_addr, address=0x7B, count=2)
296296
if full_version_data is not None and not full_version_data.isError():
297-
data["firmware_version_dsp"] = int(full_version_data.registers[0])
298-
data["firmware_version_arm"] = int(full_version_data.registers[1])
297+
data["firmware_dsp"] = int(full_version_data.registers[0])
298+
data["firmware_arm"] = int(full_version_data.registers[1])
299299
except Exception:
300300
_LOGGER.debug(f"{hub.name}: attempt to read full firmware version failed data: {full_version_data}", exc_info=True)
301301
else:
302-
data.pop("firmware_version_dsp", None)
303-
data.pop("firmware_version_arm", None)
302+
data.pop("firmware_dsp", None)
303+
data.pop("firmware_arm", None)
304304

305305
return version
306306

@@ -1351,7 +1351,8 @@ def value_function_battery_capacity_gen5(initval: int, descr: Any, datadict: dic
13511351

13521352

13531353
def value_function_software_version_g2(initval: int, descr: Any, datadict: dict[str, Any]) -> str | None:
1354-
return f"DSP v2.{datadict.get('firmware_dsp')} ARM v2.{datadict.get('firmware_arm')}"
1354+
# AC/HYBRID GEN2: split registers hold raw minor versions with a fixed major of 2
1355+
return f"DSP v2.{datadict.get('firmware_dsp_minor')} ARM v2.{datadict.get('firmware_arm_minor')}"
13551356

13561357

13571358
def value_function_firmware_major_default(val: Any, default: int) -> Any:
@@ -1361,27 +1362,9 @@ def value_function_firmware_major_default(val: Any, default: int) -> Any:
13611362
def value_function_software_version_g3(initval: int, descr: Any, datadict: dict[str, Any]) -> str | None:
13621363
return (
13631364
f"DSP v{value_function_firmware_major_default(datadict.get('firmware_dsp_major'), 3)}."
1364-
f"{value_str_default(datadict.get('firmware_dsp'), '??'):>02} "
1365+
f"{value_str_default(datadict.get('firmware_dsp_minor'), '??'):>02} "
13651366
f"ARM v{value_function_firmware_major_default(datadict.get('firmware_arm_major'), 3)}."
1366-
f"{value_str_default(datadict.get('firmware_arm'), '??'):>02}"
1367-
)
1368-
1369-
1370-
def value_function_software_version_g4(initval: int, descr: Any, datadict: dict[str, Any]) -> str | None:
1371-
return (
1372-
f"DSP {value_str_default(datadict.get('firmware_dsp_major'), '?')}."
1373-
f"{value_str_default(datadict.get('firmware_dsp'), '??'):>02} "
1374-
f"ARM {value_str_default(datadict.get('firmware_arm_major'), '?')}."
1375-
f"{value_str_default(datadict.get('firmware_arm'), '??'):>02}"
1376-
)
1377-
1378-
1379-
def value_function_software_version_g5(initval: int, descr: Any, datadict: dict[str, Any]) -> str | None:
1380-
return (
1381-
f"DSP {value_str_default(datadict.get('firmware_dsp_major'), '???'):>03}."
1382-
f"{value_str_default(datadict.get('firmware_dsp'), '??'):>02} "
1383-
f"ARM {value_str_default(datadict.get('firmware_arm_major'), '???'):>03}."
1384-
f"{value_str_default(datadict.get('firmware_arm'), '??'):>02}"
1367+
f"{value_str_default(datadict.get('firmware_arm_minor'), '??'):>02}"
13851368
)
13861369

13871370

@@ -1392,31 +1375,68 @@ def value_function_modbus_protocol_version(datadict: dict[str, Any]) -> int:
13921375
return 0
13931376

13941377

1395-
def value_function_software_version_full(initval: int, descr: Any, datadict: dict[str, Any]) -> str | None:
1396-
dsp = datadict.get("firmware_version_dsp")
1397-
arm = datadict.get("firmware_version_arm")
1398-
dsp_str = f"{dsp // 100}.{dsp % 100:02d}" if dsp is not None else "?.??"
1399-
arm_str = f"{arm // 100}.{arm % 100:02d}" if arm is not None else "?.??"
1378+
def value_function_software_version_mic(initval: int, descr: Any, datadict: dict[str, Any]) -> str | None:
1379+
"""MIC firmware version. firmware_dsp / firmware_arm hold the full version ÷100 encoded.
1380+
1381+
GEN1 — 0x33D/0x33E GEN2 — 0x352/0x353 (+ 0x354 ARM boot, ÷100) GEN4 — 0x394/0x390
1382+
Example (X3-MIC/PRO-G2): 136 → "DSP 1.36 ARM 1.36-1.00", matching SolaX Cloud.
1383+
"""
1384+
dsp = datadict.get("firmware_dsp") or 0
1385+
arm = datadict.get("firmware_arm") or 0
1386+
dsp_str = f"{dsp // 100}.{dsp % 100:02d}" if dsp else "?.??"
1387+
arm_str = f"{arm // 100}.{arm % 100:02d}" if arm else "?.??"
1388+
arm_boot = datadict.get("firmware_arm_boot")
1389+
if arm_boot is not None:
1390+
arm_str += f"-{arm_boot // 100}.{arm_boot % 100:02d}" # 0x354 is ÷100 encoded
14001391
return f"DSP {dsp_str} ARM {arm_str}"
14011392

14021393

1403-
def value_function_software_version_protocol_aware(initval: int, descr: Any, datadict: dict[str, Any]) -> str | None:
1404-
# FirmwareVersionModbus 100 means V001.00; newer protocol versions can use the combined 0x7B/0x7C registers.
1405-
if (
1406-
value_function_modbus_protocol_version(datadict) >= 100
1407-
and datadict.get("firmware_version_dsp") is not None
1408-
and datadict.get("firmware_version_arm") is not None
1409-
):
1410-
return value_function_software_version_full(initval, descr, datadict)
1411-
return value_function_software_version_g4(initval, descr, datadict)
1394+
def value_function_software_version_hybrid_legacy(initval: int, descr: Any, datadict: dict[str, Any]) -> str | None:
1395+
"""AC/HYBRID with modbus protocol < 100: version is major.minor from split registers.
1396+
1397+
DSP = firmware_dsp_major (0x7F) . firmware_dsp_minor (0x7D)
1398+
ARM = firmware_arm_major (0x80) . firmware_arm_minor (0x83), boot suffix from
1399+
bootloader_version (0x84, raw minor) — e.g. "ARM 1.58-1.15" (SolaX Cloud).
1400+
"""
1401+
dsp_maj = datadict.get("firmware_dsp_major")
1402+
dsp_min = datadict.get("firmware_dsp_minor")
1403+
arm_maj = datadict.get("firmware_arm_major")
1404+
arm_min = datadict.get("firmware_arm_minor")
1405+
dsp_str = f"{dsp_maj}.{dsp_min:02d}" if dsp_maj is not None and dsp_min is not None else "?.??"
1406+
arm_str = f"{arm_maj}.{arm_min:02d}" if arm_maj is not None and arm_min is not None else "?.??"
1407+
arm_boot = datadict.get("bootloader_version")
1408+
if arm_maj is not None and arm_boot is not None:
1409+
arm_str += f"-{arm_maj}.{arm_boot:02d}"
1410+
return f"DSP {dsp_str} ARM {arm_str}"
14121411

14131412

1414-
def value_function_software_version_air_g3(initval: int, descr: Any, datadict: dict[str, Any]) -> str | None:
1415-
return f"DSP v2.{datadict.get('firmware_dsp')} ARM v1.{datadict.get('firmware_arm')}"
1413+
def value_function_software_version_hybrid_full(initval: int, descr: Any, datadict: dict[str, Any]) -> str | None:
1414+
"""AC/HYBRID with modbus protocol >= 100: full version ÷100 from combined registers.
14161415
1416+
DSP = firmware_dsp (0x7B), ARM = firmware_arm (0x7C), both ÷100 encoded.
1417+
ARM boot suffix from firmware_arm_major (0x80) + bootloader_version (0x84, raw minor).
1418+
"""
1419+
dsp = datadict.get("firmware_dsp")
1420+
arm = datadict.get("firmware_arm")
1421+
dsp_str = f"{dsp // 100}.{dsp % 100:02d}" if dsp else "?.??"
1422+
arm_str = f"{arm // 100}.{arm % 100:02d}" if arm else "?.??"
1423+
arm_maj = datadict.get("firmware_arm_major")
1424+
arm_boot = datadict.get("bootloader_version")
1425+
if arm_maj is not None and arm_boot is not None:
1426+
arm_str += f"-{arm_maj}.{arm_boot:02d}"
1427+
return f"DSP {dsp_str} ARM {arm_str}"
14171428

1418-
def value_function_software_version_air_g4(initval: int, descr: Any, datadict: dict[str, Any]) -> str | None:
1419-
return f"DSP {datadict.get('firmware_dsp')} ARM {datadict.get('firmware_arm')}"
1429+
1430+
def value_function_software_version(initval: int, descr: Any, datadict: dict[str, Any]) -> str | None:
1431+
"""AC/HYBRID dispatcher: pick the full or legacy formatter by modbus protocol version.
1432+
1433+
Protocol >= 100 exposes the combined full-version registers (0x7B/0x7C); older
1434+
firmware only has the split major/minor registers.
1435+
"""
1436+
proto = datadict.get("modbus_protocol_version") or 0
1437+
if proto >= 100 and datadict.get("firmware_dsp") is not None:
1438+
return value_function_software_version_hybrid_full(initval, descr, datadict)
1439+
return value_function_software_version_hybrid_legacy(initval, descr, datadict)
14201440

14211441

14221442
def value_function_battery_voltage_cell_difference(initval: int, descr: Any, datadict: dict[str, Any]) -> int | float:
@@ -3877,21 +3897,21 @@ def value_function_battery_voltage_cell_difference(initval: int, descr: Any, dat
38773897
icon="mdi:solar-power-variant",
38783898
),
38793899
SolaXModbusSensorEntityDescription(
3880-
key="firmware_version_dsp",
3900+
key="firmware_dsp",
38813901
register=0x7B,
38823902
allowedtypes=AC | HYBRID | GEN4 | GEN5 | GEN6,
38833903
modbus_min=100,
38843904
internal=True,
38853905
),
38863906
SolaXModbusSensorEntityDescription(
3887-
key="firmware_version_arm",
3907+
key="firmware_arm",
38883908
register=0x7C,
38893909
allowedtypes=AC | HYBRID | GEN4 | GEN5 | GEN6,
38903910
modbus_min=100,
38913911
internal=True,
38923912
),
38933913
SolaXModbusSensorEntityDescription(
3894-
key="firmware_dsp",
3914+
key="firmware_dsp_minor",
38953915
register=0x7D,
38963916
allowedtypes=AC | HYBRID,
38973917
internal=True,
@@ -3925,7 +3945,7 @@ def value_function_battery_voltage_cell_difference(initval: int, descr: Any, dat
39253945
icon="mdi:information",
39263946
),
39273947
SolaXModbusSensorEntityDescription(
3928-
key="firmware_arm",
3948+
key="firmware_arm_minor",
39293949
register=0x83,
39303950
allowedtypes=AC | HYBRID,
39313951
internal=True,
@@ -9323,7 +9343,7 @@ def value_function_battery_voltage_cell_difference(initval: int, descr: Any, dat
93239343
SolaXModbusSensorEntityDescription(
93249344
name="Software Version",
93259345
key="software_version",
9326-
value_function=value_function_software_version_protocol_aware,
9346+
value_function=value_function_software_version,
93279347
allowedtypes=AC | HYBRID | GEN4 | GEN5 | GEN6,
93289348
entity_category=EntityCategory.DIAGNOSTIC,
93299349
icon="mdi:information",
@@ -9430,6 +9450,12 @@ def value_function_battery_voltage_cell_difference(initval: int, descr: Any, dat
94309450
allowedtypes=MIC | GEN2,
94319451
internal=True,
94329452
),
9453+
SolaXModbusSensorEntityDescription(
9454+
key="firmware_arm_boot",
9455+
register=0x354,
9456+
allowedtypes=MIC | GEN2,
9457+
internal=True,
9458+
),
94339459
SolaXModbusSensorEntityDescription(
94349460
key="lock_state",
94359461
register=0x367,
@@ -10781,38 +10807,14 @@ def value_function_battery_voltage_cell_difference(initval: int, descr: Any, dat
1078110807
SolaXModbusSensorEntityDescription(
1078210808
name="Software Version",
1078310809
key="software_version",
10784-
value_function=value_function_software_version_air_g3,
10785-
allowedtypes=MIC | GEN2 | X1,
10786-
entity_category=EntityCategory.DIAGNOSTIC,
10787-
icon="mdi:information",
10788-
),
10789-
SolaXModbusSensorEntityDescription(
10790-
name="Software Version",
10791-
key="software_version",
10792-
value_function=value_function_software_version_air_g4,
10793-
allowedtypes=MIC | GEN4 | X1,
10794-
entity_category=EntityCategory.DIAGNOSTIC,
10795-
icon="mdi:information",
10796-
),
10797-
SolaXModbusSensorEntityDescription(
10798-
name="Software Version",
10799-
key="software_version",
10800-
value_function=value_function_software_version_air_g4,
10801-
allowedtypes=MIC | GEN | X3,
10810+
value_function=value_function_software_version_mic,
10811+
allowedtypes=MIC,
1080210812
blacklist=[
1080310813
"MU802T",
1080410814
],
1080510815
entity_category=EntityCategory.DIAGNOSTIC,
1080610816
icon="mdi:information",
1080710817
),
10808-
SolaXModbusSensorEntityDescription(
10809-
name="Software Version",
10810-
key="software_version",
10811-
value_function=value_function_software_version_g2,
10812-
allowedtypes=MIC | GEN2 | X3,
10813-
entity_category=EntityCategory.DIAGNOSTIC,
10814-
icon="mdi:information",
10815-
),
1081610818
]
1081710819

1081810820
TIME_TYPES = [
@@ -11673,8 +11675,8 @@ async def async_determineInverterType(self, hub: Any, configdict: dict[str, Any]
1167311675
hub.data["hardware_version"] = value_function_hardware_version_g5(0, None, hub.data)
1167411676
elif invertertype & GEN6:
1167511677
hub.data["hardware_version"] = value_function_hardware_version_g6(0, None, hub.data)
11676-
if "firmware_dsp" in hub.data or "firmware_version_dsp" in hub.data:
11677-
hub.data["software_version"] = value_function_software_version_protocol_aware(0, None, hub.data)
11678+
if "firmware_dsp" in hub.data or "firmware_dsp_minor" in hub.data:
11679+
hub.data["software_version"] = value_function_software_version(0, None, hub.data)
1167811680

1167911681
read_eps = configdict.get(CONF_READ_EPS, DEFAULT_READ_EPS)
1168011682
read_dcb = configdict.get(CONF_READ_DCB, DEFAULT_READ_DCB)

0 commit comments

Comments
 (0)