Skip to content

Commit c26217c

Browse files
committed
fix: EVC register segmentation, firmware-version gating, doc-aligned entities
SolaX support (ticket 703158) confirmed the EVC holding registers are internally segmented: 0x600-0x650 is one segment, 0x660-0x66F another (used for master-slave parallel operation). A function 0x03 read that spans the boundary silently returns 0 for second-segment registers. Reproduced on an X3-EVC-11kW Gen1 (ARM V1.18). Fixed by starting a new read block at 0x668 and restoring the default block_size 100 (the block_size=32 workaround predating this knowledge is no longer needed). The plugin now publishes its firmware version (input 0x25, e.g. V1.18 -> 118) as hub.modbus_protocol_version, enabling modbus_min/ modbus_max gating for EV chargers. Entities on registers dated by the protocol document's revision history are gated accordingly, and every register pair that changed location across firmware versions is exposed as a single entity identity whose backing register follows the firmware: Charge Power 0x08-0x0A -> 0x100-0x102, Run Mode 0x1D -> 0x106, Charge Phase 0x625 -> 0x63B, Charge Mode (0x104 sensor on V1.12-13, 0x641 select from V1.14, 0x669 select on GEN2). The Alt-suffixed duplicate entities are gone. Support suggested using the documented 0x64F (limit_current_allmode) for the maximum current. Live testing shows it is the persistent device limit rather than the setpoint (SolaXCloud itself writes 0x668; external writes to 0x64F are acknowledged but ignored), so Max Charge Current stays on 0x668 and a new optional max_key field on number descriptions caps the Max/Min Charge Current sliders dynamically from the 0x64F readback. New doc-based entities (disabled by default unless noted): Unbalanced Power 0x63C and Unbalanced Switch 0x63D (X3), Mode Button 0x63E, Min Charge Current 0x63F, Datahub Charge Power 0x643, Green 30s Delay 0x644 (switch), Parallel Support 0x107, WiFi S/N 0x607, Customized S/N 0x629. Alignments: Overload Limit renamed Overvoltage Limit (doc OverVoltSet), Charger Type name unified across GEN1/GEN2, RSSI renamed WiFi RSSI, per-phase Charge Current sensors consistently disabled by default, Datahub Charge Current disabled by default, charge_added_cum GEN2 scope narrowed from GEN2|GEN3|GEN4. Input 0x2D/0x2E are marked in code as conflicting between the Gen1 V3.5 and GEN2 documents (clarification requested from SolaX). Register 0x642 Parallel_enable is deliberately not exposed: the device returns values outside the documented 0/1 range and SolaXCloud's parallel toggle does not use it.
1 parent 1b83570 commit c26217c

15 files changed

Lines changed: 1404 additions & 199 deletions

File tree

custom_components/solax_modbus/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@
102102
from .const import (
103103
CONF_READ_DCB as CONF_READ_DCB,
104104
)
105+
from .const import (
106+
CONF_READ_EMS as CONF_READ_EMS,
107+
)
105108
from .const import (
106109
CONF_READ_EPS as CONF_READ_EPS,
107110
)
@@ -126,6 +129,9 @@
126129
from .const import (
127130
DEFAULT_READ_DCB as DEFAULT_READ_DCB,
128131
)
132+
from .const import (
133+
DEFAULT_READ_EMS as DEFAULT_READ_EMS,
134+
)
129135
from .const import (
130136
DEFAULT_READ_EPS as DEFAULT_READ_EPS,
131137
)
@@ -447,11 +453,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
447453
DEVICE_GROUP_NAMES: dict[str, str] = {
448454
"eps": "EPS",
449455
"pm": "Parallel",
456+
"ems": "EMS∕V2G",
450457
}
451458

452459
GATED_DEVICE_GROUPS: dict[str, tuple[str, bool]] = {
453460
"eps": (CONF_READ_EPS, DEFAULT_READ_EPS),
454461
"pm": (CONF_READ_PM, DEFAULT_READ_PM),
462+
"ems": (CONF_READ_EMS, DEFAULT_READ_EMS),
455463
"ENERGY_DASHBOARD": (CONF_ENERGY_DASHBOARD_DEVICE, DEFAULT_ENERGY_DASHBOARD_DEVICE),
456464
}
457465

custom_components/solax_modbus/config_flow.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
CONF_PLUGIN,
4646
CONF_READ_BATTERY,
4747
CONF_READ_DCB,
48+
CONF_READ_EMS,
4849
CONF_READ_EPS,
4950
CONF_READ_PM,
5051
CONF_SCAN_INTERVAL_FAST,
@@ -63,6 +64,7 @@
6364
DEFAULT_PORT,
6465
DEFAULT_READ_BATTERY,
6566
DEFAULT_READ_DCB,
67+
DEFAULT_READ_EMS,
6668
DEFAULT_READ_EPS,
6769
DEFAULT_READ_PM,
6870
DEFAULT_SCAN_INTERVAL,
@@ -156,6 +158,7 @@ def _configured_hub_names(handler: SchemaCommonFlowHandler) -> set[str]:
156158
vol.Optional(CONF_READ_EPS, default=DEFAULT_READ_EPS): bool,
157159
vol.Optional(CONF_READ_DCB, default=DEFAULT_READ_DCB): bool,
158160
vol.Optional(CONF_READ_PM, default=DEFAULT_READ_PM): bool,
161+
vol.Optional(CONF_READ_EMS, default=DEFAULT_READ_EMS): bool,
159162
vol.Optional(CONF_TIME_OUT, default=DEFAULT_TIME_OUT): int,
160163
}
161164
)
@@ -178,6 +181,7 @@ def _configured_hub_names(handler: SchemaCommonFlowHandler) -> set[str]:
178181
vol.Optional(CONF_READ_EPS, default=DEFAULT_READ_EPS): bool,
179182
vol.Optional(CONF_READ_DCB, default=DEFAULT_READ_DCB): bool,
180183
vol.Optional(CONF_READ_PM, default=DEFAULT_READ_PM): bool,
184+
vol.Optional(CONF_READ_EMS, default=DEFAULT_READ_EMS): bool,
181185
vol.Optional(CONF_TIME_OUT, default=DEFAULT_TIME_OUT): int,
182186
}
183187
)
@@ -381,6 +385,8 @@ async def _option_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
381385
for option, flag_name in ((CONF_READ_EPS, "EPS"), (CONF_READ_PM, "PM")):
382386
if not _plugin_uses_feature_flag(plugin_name, flag_name):
383387
hidden.add(option)
388+
if not _plugin_supports_device_group(plugin_name, "ems"):
389+
hidden.add(CONF_READ_EMS)
384390
if not hidden:
385391
return OPTION_SCHEMA
386392
return vol.Schema({marker: value for marker, value in OPTION_SCHEMA.schema.items() if getattr(marker, "schema", None) not in hidden})

custom_components/solax_modbus/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class UnitOfReactivePower(StrEnum): # type: ignore[no-redef]
5151
CONF_READ_EPS = "read_eps"
5252
CONF_READ_DCB = "read_dcb"
5353
CONF_READ_PM = "read_pm"
54+
CONF_READ_EMS = "read_ems"
5455
CONF_MODBUS_ADDR = "read_modbus_addr"
5556
CONF_INTERFACE = "interface"
5657
CONF_SERIAL_PORT = "read_serial_port"
@@ -67,6 +68,7 @@ class UnitOfReactivePower(StrEnum): # type: ignore[no-redef]
6768
DEFAULT_READ_EPS = False
6869
DEFAULT_READ_DCB = False
6970
DEFAULT_READ_PM = False
71+
DEFAULT_READ_EMS = False
7072
DEFAULT_BAUDRATE = "19200"
7173
DEFAULT_PLUGIN = "solax"
7274
DEFAULT_READ_BATTERY = False

custom_components/solax_modbus/number.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,10 @@ async def async_set_native_value(self, value: float) -> None:
290290
f"writing {self._platform_name} {self._key} number register {self._register} value {pl} after div by readscale {self.entity_description.read_scale} scale {self._attr_scale} with mode {self._write_method}"
291291
)
292292
await self._hub.async_write_registers_multi(unit=self._modbus_addr, address=self._register, payload=pl)
293+
if self._write_method in (WRITE_MULTISINGLE_MODBUS, WRITE_SINGLE_MODBUS, WRITE_MULTI_MODBUS):
294+
# Publish the written value locally: the readback register only catches up on a
295+
# later poll, so the entity would otherwise keep reporting the previous value.
296+
self._hub.data[self._key] = value
293297
elif self._write_method == WRITE_DATA_LOCAL:
294298
_LOGGER.info(f"*** local data written {self._key}: {payload}")
295299
# corresponding_sensor = self._hub.preventSensors.get(self.entity_description.key, None)

0 commit comments

Comments
 (0)