Skip to content

Commit cb26870

Browse files
authored
Merge pull request #2160 from Bl00d-B0b/fix/consistent-entity-naming
fix: standardize entity and device naming across all platforms
2 parents 7565d1d + b003cc3 commit cb26870

8 files changed

Lines changed: 41 additions & 65 deletions

File tree

custom_components/solax_modbus/__init__.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -675,14 +675,16 @@ async def async_init(self, *args: Any) -> None: # noqa: D102
675675
return
676676

677677
# Prepare device_info (inverter detected during initial window)
678-
plugin_name = self.plugin.plugin_name
678+
# Device name = hub name + optional suffix (e.g. "EV" + "Charger" -> "EV Charger").
679+
# Unique per config entry; entity names never repeat it, HA composes the friendly name.
680+
device_name = self._name
679681
if self.inverterNameSuffix is not None and self.inverterNameSuffix != "":
680-
plugin_name = plugin_name + " " + self.inverterNameSuffix
682+
device_name = device_name + " " + self.inverterNameSuffix
681683
self.device_info = DeviceInfo(
682684
identifiers=cast(set[tuple[str, str]], {(DOMAIN, self._name, INVERTER_IDENT)}),
683685
manufacturer=self.plugin.plugin_manufacturer,
684686
model=self._get_inverter_model(),
685-
name=plugin_name,
687+
name=device_name,
686688
serial_number=self.seriesnumber,
687689
sw_version=self.plugin.getSoftwareVersion(self.data),
688690
hw_version=self.plugin.getHardwareVersion(self.data),
@@ -732,14 +734,14 @@ async def _deferred_setup_loop(self, interval: int = 30) -> None:
732734
self._invertertype = inv
733735
_LOGGER.debug(f"{self._name}: inverter detected during deferred setup (type={inv}) – forwarding platforms")
734736
# Prepare/refresh device_info in case it wasn't set
735-
plugin_name = self.plugin.plugin_name
737+
device_name = self._name
736738
if self.inverterNameSuffix:
737-
plugin_name = plugin_name + " " + self.inverterNameSuffix
739+
device_name = device_name + " " + self.inverterNameSuffix
738740
self.device_info = DeviceInfo(
739741
identifiers=cast(set[tuple[str, str]], {(DOMAIN, self._name, INVERTER_IDENT)}),
740742
manufacturer=self.plugin.plugin_manufacturer,
741743
model=self._get_inverter_model(),
742-
name=plugin_name,
744+
name=device_name,
743745
serial_number=self.seriesnumber,
744746
sw_version=self.plugin.getSoftwareVersion(self.data),
745747
hw_version=self.plugin.getHardwareVersion(self.data),

custom_components/solax_modbus/button.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
from dataclasses import replace
32
from time import time
43
from typing import Any
54

@@ -36,17 +35,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
3635
hub = hass.data[DOMAIN][hub_name]["hub"]
3736

3837
plugin = hub.plugin
39-
inverter_name_suffix = ""
40-
if hub.inverterNameSuffix is not None and hub.inverterNameSuffix != "":
41-
inverter_name_suffix = hub.inverterNameSuffix + " "
42-
4338
entities = []
4439
for button_info in plugin.BUTTON_TYPES:
4540
if plugin.matchInverterWithMask(
4641
hub._invertertype, button_info.allowedtypes, hub.seriesnumber, button_info.blacklist
4742
) and matches_modbus_protocol(hub, button_info):
48-
if not (button_info.name.startswith(inverter_name_suffix)):
49-
button_info = replace(button_info, name=inverter_name_suffix + button_info.name)
5043
button = SolaXModbusButton(hub_name, hub, modbus_addr, hub.device_info, button_info)
5144
entities.append(button)
5245
if button_info.key == plugin.wakeupButton():
@@ -80,6 +73,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
8073
class SolaXModbusButton(ButtonEntity):
8174
"""Representation of an SolaX Modbus button."""
8275

76+
_attr_has_entity_name = True
77+
8378
def __init__(
8479
self,
8580
platform_name: str,
@@ -105,8 +100,8 @@ def __init__(
105100

106101
@property
107102
def name(self) -> str:
108-
"""Return the name."""
109-
return f"{self._platform_name} {self._name}"
103+
"""Return the entity name (description name only — the device name provides context)."""
104+
return str(self._name or self._key)
110105

111106
@property
112107
def unique_id(self) -> str | None:

custom_components/solax_modbus/number.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
3737
hub = hass.data[DOMAIN][hub_name]["hub"]
3838

3939
plugin = hub.plugin # getPlugin(hub_name)
40-
inverter_name_suffix = ""
41-
if hub.inverterNameSuffix is not None and hub.inverterNameSuffix != "":
42-
inverter_name_suffix = hub.inverterNameSuffix + " "
43-
4440
entities = []
4541
for number_info in plugin.NUMBER_TYPES:
4642
newdescr = number_info
@@ -54,9 +50,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
5450
if plugin.matchInverterWithMask(hub._invertertype, newdescr.allowedtypes, hub.seriesnumber, newdescr.blacklist) and matches_modbus_protocol(
5551
hub, newdescr
5652
):
57-
if not (newdescr.name.startswith(inverter_name_suffix)):
58-
newdescr = replace(newdescr, name=inverter_name_suffix + newdescr.name)
59-
6053
number = SolaXModbusNumber(hub_name, hub, modbus_addr, hub.device_info, newdescr)
6154
if newdescr.write_method == WRITE_DATA_LOCAL:
6255
hub.writeLocals[newdescr.key] = newdescr
@@ -90,6 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
9083
class SolaXModbusNumber(NumberEntity):
9184
"""Representation of an SolaX Modbus number."""
9285

86+
_attr_has_entity_name = True
9387
entity_description: BaseModbusNumberEntityDescription
9488

9589
def __init__(
@@ -178,8 +172,8 @@ def should_poll(self) -> bool:
178172

179173
@property
180174
def name(self) -> str:
181-
"""Return the name."""
182-
return f"{self._platform_name} {self._name}"
175+
"""Return the entity name (description name only — the device name provides context)."""
176+
return str(self._name or self._key)
183177

184178
@property
185179
def unique_id(self) -> str | None:

custom_components/solax_modbus/select.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
3636
hub = hass.data[DOMAIN][hub_name]["hub"]
3737

3838
plugin = hub.plugin # getPlugin(hub_name)
39-
inverter_name_suffix = ""
40-
if hub.inverterNameSuffix is not None and hub.inverterNameSuffix != "":
41-
inverter_name_suffix = hub.inverterNameSuffix + " "
42-
4339
entities = []
4440
for select_info in plugin.SELECT_TYPES:
4541
if plugin.matchInverterWithMask(
4642
hub._invertertype, select_info.allowedtypes, hub.seriesnumber, select_info.blacklist
4743
) and matches_modbus_protocol(hub, select_info):
4844
select_info = replace(select_info, reverse_option_dict={v: k for k, v in select_info.option_dict.items()})
49-
if not (select_info.name.startswith(inverter_name_suffix)):
50-
select_info = replace(select_info, name=inverter_name_suffix + select_info.name)
5145
select = SolaXModbusSelect(hub_name, hub, modbus_addr, hub.device_info, select_info)
5246
if select_info.write_method == WRITE_DATA_LOCAL:
5347
if select_info.initvalue is not None:
@@ -86,6 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
8680
class SolaXModbusSelect(SelectEntity):
8781
"""Representation of an SolaX Modbus select."""
8882

83+
_attr_has_entity_name = True
8984
entity_description: BaseModbusSelectEntityDescription
9085

9186
def __init__(
@@ -170,8 +165,8 @@ def current_option(self) -> str | None:
170165

171166
@property
172167
def name(self) -> str:
173-
"""Return the name."""
174-
return f"{self._platform_name} {self._name}"
168+
"""Return the entity name (description name only — the device name provides context)."""
169+
return str(self._name or self._key)
175170

176171
@property
177172
def should_poll(self) -> bool:

custom_components/solax_modbus/sensor.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,8 @@ async def readFollowUp(old_data: Any, new_data: Any) -> bool:
159159
dev_registry.async_update_device(device.id, sw_version=sw_version, hw_version=hw_version)
160160
return True
161161

162+
# Entity names never carry the inverter suffix — the device name provides that context.
162163
inverter_name_suffix = ""
163-
# Test: Comment out to prevent adding inverter suffix to Energy Dashboard sensors
164-
# if hub.inverterNameSuffix is not None and hub.inverterNameSuffix != "":
165-
# inverter_name_suffix = hub.inverterNameSuffix + " "
166164

167165
# Check if hub initialization is complete
168166
if hub.device_info is None:
@@ -228,17 +226,16 @@ async def readFollowUp(old_data: Any, new_data: Any) -> bool:
228226
if batt_pack_serial is None:
229227
continue
230228

229+
# Battery pack device name = hub name + pack identity (unique per config entry);
230+
# entity names never repeat it, HA composes the friendly name.
231231
device_info_battery = DeviceInfo(
232232
identifiers=cast(set[tuple[str, str]], {(DOMAIN, hub_name, batt_pack_id)}),
233-
name=hub.plugin.plugin_name + f" Battery {batt_nr + 1}/{batt_pack_nr + 1}",
233+
name=f"{hub_name} Battery {batt_nr + 1}/{batt_pack_nr + 1}",
234234
manufacturer=hub.plugin.plugin_manufacturer,
235235
serial_number=batt_pack_serial,
236236
via_device=cast(tuple[str, str], (DOMAIN, hub_name, INVERTER_IDENT)),
237237
)
238238

239-
name_prefix = battery_config.battery_sensor_name_prefix.replace("{batt-nr}", str(batt_nr + 1)).replace(
240-
"{pack-nr}", str(batt_pack_nr + 1)
241-
)
242239
key_prefix = battery_config.battery_sensor_key_prefix.replace("{batt-nr}", str(batt_nr + 1)).replace(
243240
"{pack-nr}", str(batt_pack_nr + 1)
244241
)
@@ -275,7 +272,7 @@ async def readFollowUpBattery(
275272
computedRegs,
276273
device_info_battery,
277274
battery_config.battery_sensor_type,
278-
name_prefix,
275+
"", # entity names never carry the pack prefix — the battery device name provides it
279276
key_prefix,
280277
readPreparation,
281278
readFollowUpBattery,
@@ -553,6 +550,8 @@ async def async_refresh_energy_dashboard_entities() -> None:
553550
class SolaXModbusSensor(SensorEntity):
554551
"""Representation of an SolaX Modbus sensor."""
555552

553+
_attr_has_entity_name = True
554+
556555
def __init__(
557556
self,
558557
platform_name: str,
@@ -617,10 +616,8 @@ def _update_state(self) -> None: # never called ?????
617616

618617
@property
619618
def name(self) -> str:
620-
"""Return the name."""
621-
if self.entity_description.key in COMMUNICATION_SENSOR_KEYS:
622-
return str(self.entity_description.name or self.entity_description.key)
623-
return f"{self._platform_name} {self.entity_description.name}"
619+
"""Return the entity name (description name only — the device name provides context)."""
620+
return str(self.entity_description.name or self.entity_description.key)
624621

625622
@property
626623
def unique_id(self) -> str | None:

custom_components/solax_modbus/switch.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
from dataclasses import replace
32
from datetime import datetime
43
from typing import Any
54

@@ -34,18 +33,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
3433
hub = hass.data[DOMAIN][hub_name]["hub"]
3534

3635
plugin = hub.plugin # getPlugin(hub_name)
37-
inverter_name_suffix = ""
38-
if hub.inverterNameSuffix is not None and hub.inverterNameSuffix != "":
39-
inverter_name_suffix = hub.inverterNameSuffix + " "
40-
4136
entities = []
4237

4338
for switch_info in plugin.SWITCH_TYPES:
4439
if plugin.matchInverterWithMask(
4540
hub._invertertype, switch_info.allowedtypes, hub.seriesnumber, switch_info.blacklist
4641
) and matches_modbus_protocol(hub, switch_info):
47-
if not (switch_info.name.startswith(inverter_name_suffix)):
48-
switch_info = replace(switch_info, name=inverter_name_suffix + switch_info.name)
4942
switch = SolaXModbusSwitch(hub_name, hub, modbus_addr, hub.device_info, switch_info)
5043
if switch_info.value_function:
5144
hub.computedSwitches[switch_info.key] = switch_info
@@ -108,6 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
108101
class SolaXModbusSwitch(SwitchEntity, RestoreEntity):
109102
"""Representation of an SolaX Modbus switch."""
110103

104+
_attr_has_entity_name = True
111105
entity_description: BaseModbusSwitchEntityDescription
112106

113107
def __init__(
@@ -224,6 +218,11 @@ def is_on(self) -> bool | None:
224218

225219
return self._attr_is_on
226220

221+
@property
222+
def name(self) -> str:
223+
"""Return the entity name (description name only — the device name provides context)."""
224+
return str(self._name or self._key)
225+
227226
@property
228227
def unique_id(self) -> str | None:
229228
return f"{self._platform_name}_{self._key}"

custom_components/solax_modbus/time.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
from dataclasses import replace
32
from datetime import datetime
43
from datetime import time as datetime_time
54
from typing import Any
@@ -35,17 +34,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
3534
hub = hass.data[DOMAIN][hub_name]["hub"]
3635

3736
plugin = hub.plugin # getPlugin(hub_name)
38-
inverter_name_suffix = ""
39-
if hub.inverterNameSuffix is not None and hub.inverterNameSuffix != "":
40-
inverter_name_suffix = hub.inverterNameSuffix + " "
41-
4237
entities = []
4338
for time_info in plugin.TIME_TYPES:
4439
if plugin.matchInverterWithMask(hub._invertertype, time_info.allowedtypes, hub.seriesnumber, time_info.blacklist) and matches_modbus_protocol(
4540
hub, time_info
4641
):
47-
if not (time_info.name.startswith(inverter_name_suffix)):
48-
time_info = replace(time_info, name=inverter_name_suffix + time_info.name)
4942
time_entity = SolaXModbusTimeEntity(hub_name, hub, modbus_addr, hub.device_info, time_info)
5043
if time_info.write_method == WRITE_DATA_LOCAL:
5144
if time_info.initvalue is not None:
@@ -60,6 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
6053
class SolaXModbusTimeEntity(TimeEntity):
6154
"""Representation of an SolaX Modbus time entity."""
6255

56+
_attr_has_entity_name = True
6357
entity_description: BaseModbusTimeEntityDescription
6458

6559
def __init__(
@@ -179,16 +173,16 @@ def native_value(self) -> datetime_time | None:
179173
"""
180174
return self._attr_native_value
181175

182-
@property
183-
def name(self) -> str:
184-
"""Return the name."""
185-
return f"{self._platform_name} {self._name}"
186-
187176
@property
188177
def should_poll(self) -> bool:
189178
"""Data is delivered by by the hub"""
190179
return False
191180

181+
@property
182+
def name(self) -> str:
183+
"""Return the entity name (description name only — the device name provides context)."""
184+
return str(self._name or self._key)
185+
192186
@property
193187
def unique_id(self) -> str | None:
194188
return f"{self._platform_name}_{self._key}"

tests/unit/test_device_info_regressions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,11 @@ def test_initial_setup_device_info_pattern(self) -> None:
228228

229229
# Find both device_info initialization patterns
230230
# Pattern 1: Initial setup (in async_init_hub around line 640)
231-
initial_pattern = r"plugin_name = self\.plugin\.plugin_name\s+if self\.inverterNameSuffix.*?self\.device_info = DeviceInfo\("
231+
initial_pattern = r"device_name = self\._name\s+if self\.inverterNameSuffix.*?self\.device_info = DeviceInfo\("
232232
initial_match = re.search(initial_pattern, content, re.DOTALL)
233233

234234
# Pattern 2: Deferred setup (in _deferred_setup_loop around line 687)
235-
deferred_pattern = r"plugin_name = self\.plugin\.plugin_name\s+if self\.inverterNameSuffix.*?self\.device_info = DeviceInfo\("
235+
deferred_pattern = r"device_name = self\._name\s+if self\.inverterNameSuffix.*?self\.device_info = DeviceInfo\("
236236
deferred_matches = list(re.finditer(deferred_pattern, content, re.DOTALL))
237237

238238
assert len(deferred_matches) >= 1, "Could not find device_info initialization patterns"

0 commit comments

Comments
 (0)