Skip to content

Commit c553e92

Browse files
committed
feat: add support for S6-GR1P(2.5-6)K string inverter and implement power limit functionality
1 parent d5b6683 commit c553e92

File tree

14 files changed

+390
-158
lines changed

14 files changed

+390
-158
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ All Solis hybrid inverters should be supported, although the integration has bee
1919
* S6-EH3P(8-15)K02-NV-YD-L, model "3331"
2020
* RHI-3P(3-10)K-HVES-5G, model "CA"
2121

22+
The integration also supports the following Solis string inverters:
23+
24+
* S6-GR1P(2.5-6)K, model "0200"
25+
2226
> [!NOTE]
2327
> If your inverter is not listed here, please open an issue on GitHub using "New Solis Inverter Support Request" template.
2428

custom_components/solis_cloud_control/inverters/inverter.py

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,11 @@ class InverterMaxExportPower:
233233
scale: float = 1
234234

235235

236+
@dataclass
237+
class InverterPowerLimit:
238+
cid: int = 15
239+
240+
236241
@dataclass
237242
class InverterBatteryReserveSOC:
238243
cid: int = 157
@@ -271,32 +276,72 @@ class InverterBatteryMaxDischargeCurrent:
271276
@dataclass
272277
class Inverter:
273278
info: InverterInfo
274-
storage_mode: InverterStorageMode = field(default_factory=InverterStorageMode)
275-
charge_discharge_settings: InverterChargeDischargeSettings = field(default_factory=InverterChargeDischargeSettings)
276-
charge_discharge_slots: InverterChargeDischargeSlots = field(default_factory=InverterChargeDischargeSlots)
277-
max_export_power: InverterMaxExportPower = field(default_factory=InverterMaxExportPower)
278-
battery_reserve_soc: InverterBatteryReserveSOC = field(default_factory=InverterBatteryReserveSOC)
279-
battery_over_discharge_soc: InverterBatteryOverDischargeSOC = field(default_factory=InverterBatteryOverDischargeSOC)
280-
battery_force_charge_soc: InverterBatteryForceChargeSOC = field(default_factory=InverterBatteryForceChargeSOC)
281-
battery_recovery_soc: InverterBatteryRecoverySOC = field(default_factory=InverterBatteryRecoverySOC)
282-
battery_max_charge_soc: InverterBatteryMaxChargeSOC = field(default_factory=InverterBatteryMaxChargeSOC)
283-
battery_max_charge_current: InverterBatteryMaxChargeCurrent = field(default_factory=InverterBatteryMaxChargeCurrent)
284-
battery_max_discharge_current: InverterBatteryMaxDischargeCurrent = field(
285-
default_factory=InverterBatteryMaxDischargeCurrent
286-
)
279+
storage_mode: InverterStorageMode | None = None
280+
charge_discharge_settings: InverterChargeDischargeSettings | None = None
281+
charge_discharge_slots: InverterChargeDischargeSlots | None = None
282+
max_export_power: InverterMaxExportPower | None = None
283+
power_limit: InverterPowerLimit | None = None
284+
battery_reserve_soc: InverterBatteryReserveSOC | None = None
285+
battery_over_discharge_soc: InverterBatteryOverDischargeSOC | None = None
286+
battery_force_charge_soc: InverterBatteryForceChargeSOC | None = None
287+
battery_recovery_soc: InverterBatteryRecoverySOC | None = None
288+
battery_max_charge_soc: InverterBatteryMaxChargeSOC | None = None
289+
battery_max_charge_current: InverterBatteryMaxChargeCurrent | None = None
290+
battery_max_discharge_current: InverterBatteryMaxDischargeCurrent | None = None
291+
292+
@staticmethod
293+
def create_string_inverter(
294+
inverter_info: InverterInfo,
295+
) -> "Inverter":
296+
return Inverter(
297+
info=inverter_info,
298+
power_limit=InverterPowerLimit(),
299+
)
300+
301+
@staticmethod
302+
def create_hybrid_inverter(
303+
inverter_info: InverterInfo,
304+
) -> "Inverter":
305+
return Inverter(
306+
info=inverter_info,
307+
storage_mode=InverterStorageMode(),
308+
charge_discharge_settings=InverterChargeDischargeSettings(),
309+
charge_discharge_slots=InverterChargeDischargeSlots(),
310+
max_export_power=InverterMaxExportPower(),
311+
battery_reserve_soc=InverterBatteryReserveSOC(),
312+
battery_over_discharge_soc=InverterBatteryOverDischargeSOC(),
313+
battery_force_charge_soc=InverterBatteryForceChargeSOC(),
314+
battery_recovery_soc=InverterBatteryRecoverySOC(),
315+
battery_max_charge_soc=InverterBatteryMaxChargeSOC(),
316+
battery_max_charge_current=InverterBatteryMaxChargeCurrent(),
317+
battery_max_discharge_current=InverterBatteryMaxDischargeCurrent(),
318+
)
287319

288320
@property
289321
def all_cids(self) -> list[int]:
290-
return [
291-
self.storage_mode.cid,
292-
self.charge_discharge_settings.cid,
293-
*self.charge_discharge_slots.all_cids,
294-
self.max_export_power.cid,
295-
self.battery_reserve_soc.cid,
296-
self.battery_over_discharge_soc.cid,
297-
self.battery_force_charge_soc.cid,
298-
self.battery_recovery_soc.cid,
299-
self.battery_max_charge_soc.cid,
300-
self.battery_max_charge_current.cid,
301-
self.battery_max_discharge_current.cid,
302-
]
322+
cids: list[int] = []
323+
if self.storage_mode:
324+
cids.append(self.storage_mode.cid)
325+
if self.charge_discharge_settings:
326+
cids.append(self.charge_discharge_settings.cid)
327+
if self.charge_discharge_slots:
328+
cids.extend(self.charge_discharge_slots.all_cids)
329+
if self.max_export_power:
330+
cids.append(self.max_export_power.cid)
331+
if self.power_limit:
332+
cids.append(self.power_limit.cid)
333+
if self.battery_reserve_soc:
334+
cids.append(self.battery_reserve_soc.cid)
335+
if self.battery_over_discharge_soc:
336+
cids.append(self.battery_over_discharge_soc.cid)
337+
if self.battery_force_charge_soc:
338+
cids.append(self.battery_force_charge_soc.cid)
339+
if self.battery_recovery_soc:
340+
cids.append(self.battery_recovery_soc.cid)
341+
if self.battery_max_charge_soc:
342+
cids.append(self.battery_max_charge_soc.cid)
343+
if self.battery_max_charge_current:
344+
cids.append(self.battery_max_charge_current.cid)
345+
if self.battery_max_discharge_current:
346+
cids.append(self.battery_max_discharge_current.cid)
347+
return cids

custom_components/solis_cloud_control/inverters/inverter_factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,5 @@ async def create_inverter(api_client: SolisCloudControlApiClient, inverter_info:
3434
_LOGGER.info("Inverter model '%s' created", inverter_info.model)
3535
return inverter
3636
except ImportError:
37-
_LOGGER.warning("Unknown inverter model '%s', fallback to generic inverter", inverter_info.model)
37+
_LOGGER.warning("Unknown inverter model '%s', fallback to generic hybrid inverter", inverter_info.model)
3838
return Inverter(inverter_info)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from custom_components.solis_cloud_control.api.solis_api import SolisCloudControlApiClient
2+
from custom_components.solis_cloud_control.inverters.inverter import Inverter, InverterInfo
3+
4+
5+
# S6-GR1P(2.5-6)K
6+
async def create_inverter(
7+
inverter_info: InverterInfo,
8+
api_client: SolisCloudControlApiClient, # noqa: ARG001
9+
) -> Inverter:
10+
return Inverter.create_string_inverter(inverter_info)

custom_components/solis_cloud_control/inverters/model_3331.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ async def create_inverter(
77
inverter_info: InverterInfo,
88
api_client: SolisCloudControlApiClient, # noqa: ARG001
99
) -> Inverter:
10-
inverter = Inverter(inverter_info)
10+
inverter = Inverter.create_hybrid_inverter(inverter_info)
1111

1212
power = inverter_info.power if inverter_info.power is not None else 15_000
1313
inverter.max_export_power = InverterMaxExportPower(max_value=power, step=100, scale=0.01)

custom_components/solis_cloud_control/inverters/model_ca.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ async def create_inverter(
77
inverter_info: InverterInfo,
88
api_client: SolisCloudControlApiClient, # noqa: ARG001
99
) -> Inverter:
10-
inverter = Inverter(inverter_info)
10+
inverter = Inverter.create_hybrid_inverter(inverter_info)
1111

1212
power = inverter_info.power if inverter_info.power is not None else 10_000
1313
inverter.max_export_power = InverterMaxExportPower(max_value=power)

custom_components/solis_cloud_control/number.py

Lines changed: 93 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
InverterBatteryOverDischargeSOC,
1414
InverterChargeDischargeSlot,
1515
InverterMaxExportPower,
16+
InverterPowerLimit,
1617
)
1718
from custom_components.solis_cloud_control.utils.safe_converters import safe_get_float_value
1819

@@ -34,65 +35,80 @@ async def async_setup_entry(
3435

3536
slots = inverter.charge_discharge_slots
3637

37-
for i in range(1, slots.SLOTS_COUNT + 1):
38-
entities.extend(
39-
[
40-
BatteryCurrent(
41-
coordinator=coordinator,
42-
entity_description=NumberEntityDescription(
43-
key=f"slot{i}_charge_current",
44-
name=f"Slot{i} Charge Current",
45-
icon="mdi:battery-plus-outline",
38+
if slots is not None:
39+
for i in range(1, slots.SLOTS_COUNT + 1):
40+
entities.extend(
41+
[
42+
BatteryCurrent(
43+
coordinator=coordinator,
44+
entity_description=NumberEntityDescription(
45+
key=f"slot{i}_charge_current",
46+
name=f"Slot{i} Charge Current",
47+
icon="mdi:battery-plus-outline",
48+
),
49+
charge_discharge_slot=slots.get_charge_slot(i),
50+
battery_max_charge_discharge_current=inverter.battery_max_charge_current,
4651
),
47-
charge_discharge_slot=slots.get_charge_slot(i),
48-
battery_max_charge_discharge_current=inverter.battery_max_charge_current,
49-
),
50-
BatteryCurrent(
51-
coordinator=coordinator,
52-
entity_description=NumberEntityDescription(
53-
key=f"slot{i}_discharge_current",
54-
name=f"Slot{i} Discharge Current",
55-
icon="mdi:battery-minus-outline",
52+
BatteryCurrent(
53+
coordinator=coordinator,
54+
entity_description=NumberEntityDescription(
55+
key=f"slot{i}_discharge_current",
56+
name=f"Slot{i} Discharge Current",
57+
icon="mdi:battery-minus-outline",
58+
),
59+
charge_discharge_slot=slots.get_discharge_slot(i),
60+
battery_max_charge_discharge_current=inverter.battery_max_discharge_current,
5661
),
57-
charge_discharge_slot=slots.get_discharge_slot(i),
58-
battery_max_charge_discharge_current=inverter.battery_max_discharge_current,
59-
),
60-
BatterySoc(
61-
coordinator=coordinator,
62-
entity_description=NumberEntityDescription(
63-
key=f"slot{i}_charge_soc",
64-
name=f"Slot{i} Charge SOC",
65-
icon="mdi:battery-plus-outline",
62+
BatterySoc(
63+
coordinator=coordinator,
64+
entity_description=NumberEntityDescription(
65+
key=f"slot{i}_charge_soc",
66+
name=f"Slot{i} Charge SOC",
67+
icon="mdi:battery-plus-outline",
68+
),
69+
charge_discharge_slot=slots.get_charge_slot(i),
70+
battery_over_discharge_soc=inverter.battery_over_discharge_soc,
71+
battery_max_charge_soc=inverter.battery_max_charge_soc,
6672
),
67-
charge_discharge_slot=slots.get_charge_slot(i),
68-
battery_over_discharge_soc=inverter.battery_over_discharge_soc,
69-
battery_max_charge_soc=inverter.battery_max_charge_soc,
70-
),
71-
BatterySoc(
72-
coordinator=coordinator,
73-
entity_description=NumberEntityDescription(
74-
key=f"slot{i}_discharge_soc",
75-
name=f"Slot{i} Discharge SOC",
76-
icon="mdi:battery-minus-outline",
73+
BatterySoc(
74+
coordinator=coordinator,
75+
entity_description=NumberEntityDescription(
76+
key=f"slot{i}_discharge_soc",
77+
name=f"Slot{i} Discharge SOC",
78+
icon="mdi:battery-minus-outline",
79+
),
80+
charge_discharge_slot=slots.get_discharge_slot(i),
81+
battery_over_discharge_soc=inverter.battery_over_discharge_soc,
82+
battery_max_charge_soc=inverter.battery_max_charge_soc,
7783
),
78-
charge_discharge_slot=slots.get_discharge_slot(i),
79-
battery_over_discharge_soc=inverter.battery_over_discharge_soc,
80-
battery_max_charge_soc=inverter.battery_max_charge_soc,
84+
]
85+
)
86+
87+
if inverter.max_export_power is not None:
88+
entities.append(
89+
MaxExportPower(
90+
coordinator=coordinator,
91+
entity_description=NumberEntityDescription(
92+
key="max_export_power",
93+
name="Max Export Power",
94+
icon="mdi:transmission-tower-export",
8195
),
82-
]
96+
max_export_power=inverter.max_export_power,
97+
)
8398
)
8499

85-
entities.append(
86-
MaxExportPower(
87-
coordinator=coordinator,
88-
entity_description=NumberEntityDescription(
89-
key="max_export_power",
90-
name="Max Export Power",
91-
icon="mdi:transmission-tower-export",
92-
),
93-
max_export_power=inverter.max_export_power,
100+
if inverter.power_limit is not None:
101+
entities.append(
102+
PowerLimit(
103+
coordinator=coordinator,
104+
entity_description=NumberEntityDescription(
105+
key="power_limit",
106+
name="Power Limit",
107+
icon="mdi:transmission-tower-export",
108+
),
109+
power_limit=inverter.power_limit,
110+
)
94111
)
95-
)
96112

97113
async_add_entities(entities)
98114

@@ -220,3 +236,29 @@ async def async_set_native_value(self, value: float) -> None:
220236
value_str = str(int(round(value * self.max_export_power.scale)))
221237
_LOGGER.info("Setting max export power to %f (value: %s)", value, value_str)
222238
await self.coordinator.control(self.max_export_power.cid, value_str)
239+
240+
241+
class PowerLimit(SolisCloudControlEntity, NumberEntity):
242+
def __init__(
243+
self,
244+
coordinator: SolisCloudControlCoordinator,
245+
entity_description: NumberEntityDescription,
246+
power_limit: InverterPowerLimit,
247+
) -> None:
248+
super().__init__(coordinator, entity_description, power_limit.cid)
249+
self.power_limit = power_limit
250+
251+
self._attr_native_min_value = 0
252+
self._attr_native_max_value = 100
253+
self._attr_native_step = 1
254+
self._attr_native_unit_of_measurement = PERCENTAGE
255+
256+
@property
257+
def native_value(self) -> float | None:
258+
value_str = self.coordinator.data.get(self.power_limit.cid)
259+
return safe_get_float_value(value_str)
260+
261+
async def async_set_native_value(self, value: float) -> None:
262+
value_str = str(int(round(value)))
263+
_LOGGER.info("Setting power limit to %f (value: %s)", value, value_str)
264+
await self.coordinator.control(self.power_limit.cid, value_str)

custom_components/solis_cloud_control/select.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ async def async_setup_entry(
2222
inverter = entry.runtime_data.inverter
2323
coordinator = entry.runtime_data.coordinator
2424

25-
async_add_entities(
26-
[
25+
entities = []
26+
27+
if inverter.storage_mode is not None:
28+
entities.append(
2729
StorageModeSelect(
2830
coordinator=coordinator,
2931
entity_description=SelectEntityDescription(
@@ -33,8 +35,9 @@ async def async_setup_entry(
3335
),
3436
storage_mode=inverter.storage_mode,
3537
)
36-
]
37-
)
38+
)
39+
40+
async_add_entities(entities)
3841

3942

4043
class StorageModeSelect(SolisCloudControlEntity, SelectEntity):

0 commit comments

Comments
 (0)