Skip to content

Commit 228248c

Browse files
Copilotnijelpre-commit-ci[bot]
authored
Enable multiple inverter installations (#74)
* Initial plan * Fix multiple inverter installations support - Add unique ID handling in config flow to allow multiple instances - Update all entity unique IDs to include inverter ID to prevent conflicts Co-authored-by: nijel <212189+nijel@users.noreply.github.com> * Improve sensor unique ID consistency - Store inverter_id as instance variable for consistency - Simplify unique_id appending logic with clearer comment Co-authored-by: nijel <212189+nijel@users.noreply.github.com> * Initial plan * Implement non-breaking migration strategy for entity unique IDs Co-authored-by: nijel <212189+nijel@users.noreply.github.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Address review comments: consistency and duplicate detection - Make sensor.py consistent with binary_sensor.py and switch.py by setting unique_id in each class __init__ - Add duplicate inverter_id check for old entries without unique_id - Ensures proper duplicate detection for backward compatibility Co-authored-by: nijel <212189+nijel@users.noreply.github.com> * Refactor: Extract unique ID logic to shared helper method - Add _get_unique_id helper method to each base class - Reduces code duplication across all entity __init__ methods - Simplifies maintenance and improves code readability Co-authored-by: nijel <212189+nijel@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nijel <212189+nijel@users.noreply.github.com> Co-authored-by: Michal Čihař <michal@cihar.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 5931faf commit 228248c

File tree

4 files changed

+105
-19
lines changed

4 files changed

+105
-19
lines changed

custom_components/proteus_api/binary_sensor.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,20 @@ def __init__(self, coordinator, config_entry):
4242
"""Initialize the binary sensor."""
4343
super().__init__(coordinator)
4444
self._config_entry = config_entry
45+
self._inverter_id = config_entry.data["inverter_id"]
4546
self._attr_device_info = {
4647
"identifiers": {(DOMAIN, config_entry.entry_id)},
4748
"name": "Proteus Inverter",
4849
"manufacturer": "Delta Green",
4950
"model": "Proteus",
5051
}
5152

53+
def _get_unique_id(self, base_id: str) -> str:
54+
"""Get unique ID with optional inverter_id suffix for new installations."""
55+
if self._config_entry.data.get("use_unique_id_suffix", False):
56+
return f"{base_id}_{self._inverter_id}"
57+
return base_id
58+
5259

5360
class ProteusManualControlBinarySensor(ProteusBaseBinarySensor):
5461
"""Binary sensor for manual control states."""
@@ -58,7 +65,7 @@ def __init__(self, coordinator, config_entry, control_type, friendly_name):
5865
super().__init__(coordinator, config_entry)
5966
self._control_type = control_type
6067
self._attr_name = f"Proteus {friendly_name}"
61-
self._attr_unique_id = f"proteus_{control_type.lower()}"
68+
self._attr_unique_id = self._get_unique_id(f"proteus_{control_type.lower()}")
6269
self._attr_icon = self._get_icon_for_control_type(control_type)
6370

6471
def _get_icon_for_control_type(self, control_type: str) -> str:

custom_components/proteus_api/config_flow.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,17 @@ async def async_step_user(
9797
_LOGGER.exception("Unexpected exception")
9898
errors["base"] = "unknown"
9999
else:
100+
# Check for duplicate inverter_id in existing entries
101+
# This handles both old entries (without unique_id) and new entries
102+
for entry in self._async_current_entries():
103+
if entry.data.get("inverter_id") == user_input["inverter_id"]:
104+
return self.async_abort(reason="already_configured")
105+
106+
# Set unique ID based on inverter ID to allow multiple instances
107+
await self.async_set_unique_id(user_input["inverter_id"])
108+
self._abort_if_unique_id_configured()
109+
# Add flag for new installations to use unique ID suffix
110+
user_input["use_unique_id_suffix"] = True
100111
return self.async_create_entry(title=info["title"], data=user_input)
101112

102113
return self.async_show_form(

custom_components/proteus_api/sensor.py

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,32 @@ def __init__(self, coordinator, config_entry):
5858
"""Initialize the sensor."""
5959
super().__init__(coordinator)
6060
self._config_entry = config_entry
61+
self._inverter_id = config_entry.data["inverter_id"]
6162
self._attr_device_info = {
6263
"identifiers": {(DOMAIN, config_entry.entry_id)},
6364
"name": "Proteus Inverter",
6465
"manufacturer": "Delta Green",
6566
"model": "Proteus",
6667
}
6768

69+
def _get_unique_id(self, base_id: str) -> str:
70+
"""Get unique ID with optional inverter_id suffix for new installations."""
71+
if self._config_entry.data.get("use_unique_id_suffix", False):
72+
return f"{base_id}_{self._inverter_id}"
73+
return base_id
74+
6875

6976
class ProteusFlexibilityStatusSensor(ProteusBaseSensor):
7077
"""Flexibility status sensor."""
7178

7279
_attr_name = "Proteus flexibilita dostupná"
73-
_attr_unique_id = "proteus_flex_status"
7480
_attr_icon = "mdi:lightning-bolt"
7581

82+
def __init__(self, coordinator, config_entry):
83+
"""Initialize the sensor."""
84+
super().__init__(coordinator, config_entry)
85+
self._attr_unique_id = self._get_unique_id("proteus_flex_status")
86+
7687
@property
7788
def native_value(self):
7889
"""Return the native value of the sensor."""
@@ -85,9 +96,13 @@ class ProteusModeSensor(ProteusBaseSensor):
8596
"""Mode sensor."""
8697

8798
_attr_name = "Proteus režim"
88-
_attr_unique_id = "proteus_mode"
8999
_attr_icon = "mdi:cog"
90100

101+
def __init__(self, coordinator, config_entry):
102+
"""Initialize the sensor."""
103+
super().__init__(coordinator, config_entry)
104+
self._attr_unique_id = self._get_unique_id("proteus_mode")
105+
91106
@property
92107
def native_value(self) -> str | None:
93108
"""Return the state of the sensor."""
@@ -100,9 +115,13 @@ class ProteusFlexibilityModeSensor(ProteusBaseSensor):
100115
"""Flexibility mode sensor."""
101116

102117
_attr_name = "Proteus režim flexibility"
103-
_attr_unique_id = "proteus_flexibility_mode"
104118
_attr_icon = "mdi:cog"
105119

120+
def __init__(self, coordinator, config_entry):
121+
"""Initialize the sensor."""
122+
super().__init__(coordinator, config_entry)
123+
self._attr_unique_id = self._get_unique_id("proteus_flexibility_mode")
124+
106125
@property
107126
def native_value(self) -> str | None:
108127
"""Return the state of the sensor."""
@@ -115,11 +134,15 @@ class ProteusFlexibilityTodaySensor(ProteusBaseSensor):
115134
"""Flexibility today sensor."""
116135

117136
_attr_name = "Proteus obchodování flexibility dnes"
118-
_attr_unique_id = "proteus_flexibility_today"
119137
_attr_native_unit_of_measurement = "Kč"
120138
_attr_device_class = SensorDeviceClass.MONETARY
121139
_attr_icon = "mdi:currency-czk"
122140

141+
def __init__(self, coordinator, config_entry):
142+
"""Initialize the sensor."""
143+
super().__init__(coordinator, config_entry)
144+
self._attr_unique_id = self._get_unique_id("proteus_flexibility_today")
145+
123146
@property
124147
def native_value(self) -> float | None:
125148
"""Return the state of the sensor."""
@@ -132,11 +155,15 @@ class ProteusFlexibilityMonthSensor(ProteusBaseSensor):
132155
"""Flexibility month sensor."""
133156

134157
_attr_name = "Proteus obchodování flexibility za měsíc"
135-
_attr_unique_id = "proteus_flexibility_month"
136158
_attr_native_unit_of_measurement = "Kč"
137159
_attr_device_class = SensorDeviceClass.MONETARY
138160
_attr_icon = "mdi:currency-czk"
139161

162+
def __init__(self, coordinator, config_entry):
163+
"""Initialize the sensor."""
164+
super().__init__(coordinator, config_entry)
165+
self._attr_unique_id = self._get_unique_id("proteus_flexibility_month")
166+
140167
@property
141168
def native_value(self) -> float | None:
142169
"""Return the state of the sensor."""
@@ -149,11 +176,15 @@ class ProteusFlexibilityTotalSensor(ProteusBaseSensor):
149176
"""Flexibility total sensor."""
150177

151178
_attr_name = "Proteus obchodování flexibility celkem"
152-
_attr_unique_id = "proteus_flexibility_total"
153179
_attr_native_unit_of_measurement = "Kč"
154180
_attr_device_class = SensorDeviceClass.MONETARY
155181
_attr_icon = "mdi:currency-czk"
156182

183+
def __init__(self, coordinator, config_entry):
184+
"""Initialize the sensor."""
185+
super().__init__(coordinator, config_entry)
186+
self._attr_unique_id = self._get_unique_id("proteus_flexibility_total")
187+
157188
@property
158189
def native_value(self) -> float | None:
159190
"""Return the state of the sensor."""
@@ -166,12 +197,12 @@ class ProteusCommandSensor(ProteusBaseSensor):
166197
"""Command sensor."""
167198

168199
_attr_name = "Proteus příkaz flexibility"
169-
_attr_unique_id = "proteus_command"
170200
_attr_icon = "mdi:flash"
171201

172202
def __init__(self, coordinator, config_entry):
173203
"""Initialize the sensor."""
174204
super().__init__(coordinator, config_entry)
205+
self._attr_unique_id = self._get_unique_id("proteus_command")
175206
self._cancel_time_tracker = None
176207
self._local_end_time = None
177208

@@ -297,10 +328,14 @@ class ProteusCommandEndSensor(ProteusBaseSensor):
297328
"""Command end sensor."""
298329

299330
_attr_name = "Proteus konec flexibility"
300-
_attr_unique_id = "proteus_command_end"
301331
_attr_device_class = SensorDeviceClass.TIMESTAMP
302332
_attr_icon = "mdi:clock-end"
303333

334+
def __init__(self, coordinator, config_entry):
335+
"""Initialize the sensor."""
336+
super().__init__(coordinator, config_entry)
337+
self._attr_unique_id = self._get_unique_id("proteus_command_end")
338+
304339
@property
305340
def native_value(self) -> str | None:
306341
"""Return the state of the sensor."""
@@ -313,9 +348,13 @@ class ProteusBatteryModeSensor(ProteusBaseSensor):
313348
"""Battery mode sensor."""
314349

315350
_attr_name = "Proteus režim baterie"
316-
_attr_unique_id = "proteus_flexalgo_battery"
317351
_attr_icon = "mdi:battery"
318352

353+
def __init__(self, coordinator, config_entry):
354+
"""Initialize the sensor."""
355+
super().__init__(coordinator, config_entry)
356+
self._attr_unique_id = self._get_unique_id("proteus_flexalgo_battery")
357+
319358
@property
320359
def native_value(self) -> str | None:
321360
"""Return the state of the sensor."""
@@ -328,9 +367,13 @@ class ProteusBatteryFallbackSensor(ProteusBaseSensor):
328367
"""Battery fallback sensor."""
329368

330369
_attr_name = "Proteus záložní režim baterie"
331-
_attr_unique_id = "proteus_flexalgo_battery_fallback"
332370
_attr_icon = "mdi:battery-outline"
333371

372+
def __init__(self, coordinator, config_entry):
373+
"""Initialize the sensor."""
374+
super().__init__(coordinator, config_entry)
375+
self._attr_unique_id = self._get_unique_id("proteus_flexalgo_battery_fallback")
376+
334377
@property
335378
def native_value(self) -> str | None:
336379
"""Return the state of the sensor."""
@@ -343,9 +386,13 @@ class ProteusPvModeSensor(ProteusBaseSensor):
343386
"""PV mode sensor."""
344387

345388
_attr_name = "Proteus režim výroby"
346-
_attr_unique_id = "proteus_flexalgo_pv"
347389
_attr_icon = "mdi:solar-panel"
348390

391+
def __init__(self, coordinator, config_entry):
392+
"""Initialize the sensor."""
393+
super().__init__(coordinator, config_entry)
394+
self._attr_unique_id = self._get_unique_id("proteus_flexalgo_pv")
395+
349396
@property
350397
def native_value(self) -> str | None:
351398
"""Return the state of the sensor."""
@@ -358,10 +405,14 @@ class ProteusTargetSocSensor(ProteusBaseSensor):
358405
"""Target SoC sensor."""
359406

360407
_attr_name = "Proteus cílový SOC"
361-
_attr_unique_id = "proteus_target_soc"
362408
_attr_native_unit_of_measurement = "%"
363409
_attr_icon = "mdi:battery-charging"
364410

411+
def __init__(self, coordinator, config_entry):
412+
"""Initialize the sensor."""
413+
super().__init__(coordinator, config_entry)
414+
self._attr_unique_id = self._get_unique_id("proteus_target_soc")
415+
365416
@property
366417
def native_value(self) -> float | None:
367418
"""Return the state of the sensor."""
@@ -374,12 +425,16 @@ class ProteusPredictedProductionSensor(ProteusBaseSensor):
374425
"""Predicted production sensor."""
375426

376427
_attr_name = "Proteus odhad výroby"
377-
_attr_unique_id = "proteus_predicted_production"
378428
_attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
379429
_attr_device_class = SensorDeviceClass.ENERGY_STORAGE
380430
_attr_state_class = SensorStateClass.MEASUREMENT
381431
_attr_icon = "mdi:solar-power"
382432

433+
def __init__(self, coordinator, config_entry):
434+
"""Initialize the sensor."""
435+
super().__init__(coordinator, config_entry)
436+
self._attr_unique_id = self._get_unique_id("proteus_predicted_production")
437+
383438
@property
384439
def native_value(self) -> float | None:
385440
"""Return the state of the sensor."""
@@ -392,12 +447,16 @@ class ProteusPredictedConsumptionSensor(ProteusBaseSensor):
392447
"""Predicted consumption sensor."""
393448

394449
_attr_name = "Proteus odhad spotřeby"
395-
_attr_unique_id = "proteus_predicted_consumption"
396450
_attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
397451
_attr_device_class = SensorDeviceClass.ENERGY_STORAGE
398452
_attr_state_class = SensorStateClass.MEASUREMENT
399453
_attr_icon = "mdi:home-lightning-bolt"
400454

455+
def __init__(self, coordinator, config_entry):
456+
"""Initialize the sensor."""
457+
super().__init__(coordinator, config_entry)
458+
self._attr_unique_id = self._get_unique_id("proteus_predicted_consumption")
459+
401460
@property
402461
def native_value(self) -> float | None:
403462
"""Return the state of the sensor."""

custom_components/proteus_api/switch.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,20 @@ def __init__(self, coordinator, config_entry, api):
5252
super().__init__(coordinator)
5353
self._config_entry = config_entry
5454
self._api = api
55+
self._inverter_id = config_entry.data["inverter_id"]
5556
self._attr_device_info = {
5657
"identifiers": {(DOMAIN, config_entry.entry_id)},
5758
"name": "Proteus Inverter",
5859
"manufacturer": "Delta Green",
5960
"model": "Proteus",
6061
}
6162

63+
def _get_unique_id(self, base_id: str) -> str:
64+
"""Get unique ID with optional inverter_id suffix for new installations."""
65+
if self._config_entry.data.get("use_unique_id_suffix", False):
66+
return f"{base_id}_{self._inverter_id}"
67+
return base_id
68+
6269

6370
class ProteusManualControlSwitch(ProteusBaseSwitch):
6471
"""Switch for manual control states."""
@@ -68,7 +75,9 @@ def __init__(self, coordinator, config_entry, api, control_type, friendly_name):
6875
super().__init__(coordinator, config_entry, api)
6976
self._control_type = control_type
7077
self._attr_name = f"Proteus {friendly_name}"
71-
self._attr_unique_id = f"proteus_switch_{control_type.lower()}"
78+
self._attr_unique_id = self._get_unique_id(
79+
f"proteus_switch_{control_type.lower()}"
80+
)
7281
self._attr_icon = self._get_icon_for_control_type(control_type)
7382

7483
def _get_icon_for_control_type(self, control_type: str) -> str:
@@ -128,7 +137,7 @@ def __init__(self, coordinator, config_entry, api):
128137
"""Initialize the switch."""
129138
super().__init__(coordinator, config_entry, api)
130139
self._attr_name = "Proteus řízení FVE"
131-
self._attr_unique_id = "proteus_switch_control_enabled"
140+
self._attr_unique_id = self._get_unique_id("proteus_switch_control_enabled")
132141
self._attr_icon = "mdi:network"
133142

134143
@property
@@ -166,7 +175,7 @@ def __init__(self, coordinator, config_entry, api):
166175
"""Initialize the switch."""
167176
super().__init__(coordinator, config_entry, api)
168177
self._attr_name = "Proteus optimalizace algoritmem"
169-
self._attr_unique_id = "proteus_switch_automatic_mode"
178+
self._attr_unique_id = self._get_unique_id("proteus_switch_automatic_mode")
170179
self._attr_icon = "mdi:creation"
171180

172181
@property
@@ -211,7 +220,7 @@ def __init__(self, coordinator, config_entry, api):
211220
"""Initialize the switch."""
212221
super().__init__(coordinator, config_entry, api)
213222
self._attr_name = "Proteus obchodování flexibility"
214-
self._attr_unique_id = "proteus_switch_flexibility_mode"
223+
self._attr_unique_id = self._get_unique_id("proteus_switch_flexibility_mode")
215224
self._attr_icon = "mdi:robot"
216225

217226
@property

0 commit comments

Comments
 (0)