Skip to content

Commit 5120dbf

Browse files
committed
feat: add Max Output Power control for hybrid inverters
1 parent eb37f6e commit 5120dbf

File tree

6 files changed

+118
-12
lines changed

6 files changed

+118
-12
lines changed

README.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ After successful configuration, the integration creates a new entity for your in
4242

4343
All Solis inverters should be supported, although the integration has been tested with the following models:
4444

45-
| Model Name | Model Id | Hybrid |
45+
| Model Name | Model Id | Type |
4646
| ------------------------ | -------- | ------ |
47-
| S6-EH3P(8-15)K02-NV-YD-L | 3331 | |
48-
| S6-EH3P(5-10)K-H | 3306 | |
49-
| RHI-3P(3-10)K-HVES-5G | CA | |
50-
| S6-GR1P(2.5-6)K | 0200 | |
51-
| S5-GR3P(3-20)K | 0507 | |
47+
| S6-EH3P(8-15)K02-NV-YD-L | 3331 | hybrid |
48+
| S6-EH3P(5-10)K-H | 3306 | hybrid |
49+
| RHI-3P(3-10)K-HVES-5G | CA | hybrid |
50+
| S6-GR1P(2.5-6)K | 0200 | string |
51+
| S5-GR3P(3-20)K | 0507 | string |
5252

5353
> [!NOTE]
5454
> If your inverter is not listed above, please open a GitHub issue using the "New Solis Inverter Support Request" template.
@@ -58,11 +58,17 @@ All Solis inverters should be supported, although the integration has been teste
5858

5959
The integration provides a user-friendly interface to control your inverter settings. It allows you to:
6060

61-
* ⚡ Control storage modes: "Self-Use", "Feed-In Priority" and "Off-Grid" (hybrid inverters only)
62-
* ⏱️ Schedule charge and discharge slots (hybrid inverters only)
63-
* 🛠️ "Battery Reserve" and "Allow Grid Charging" switches (hybrid inverters only)
64-
* ⚖️ Set maximum export power
65-
* 🔌 Switch the inverter on or off
61+
* ⚡ Control storage modes: "Self-Use", "Feed-In Priority" and "Off-Grid" 🟢
62+
* ⏱️ Schedule charge and discharge slots 🟢
63+
* Switch the inverter on or off 🟢 ⚪️
64+
* Toggle "Battery Reserve" 🟢
65+
* Toggle "Allow Grid Charging" 🟢
66+
* Set maximum output power 🟢
67+
* Set maximum export power 🟢
68+
* Set power limit ⚪️
69+
70+
🟢 - Hybrid inverter
71+
⚪️ - String inverter
6672

6773
![Inverter Controls](inverter_controls.png)
6874

custom_components/solis_cloud_control/inverters/inverter.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,11 @@ def get_discharge_slot(self, slot_number: int) -> InverterChargeDischargeSlot |
246246
return None
247247

248248

249+
@dataclass(frozen=True)
250+
class InverterMaxOutputPower:
251+
cid: int = 376
252+
253+
249254
@dataclass(frozen=True)
250255
class InverterMaxExportPower:
251256
cid: int = 499
@@ -302,6 +307,7 @@ class Inverter:
302307
storage_mode: InverterStorageMode | None = None
303308
charge_discharge_settings: InverterChargeDischargeSettings | None = None
304309
charge_discharge_slots: InverterChargeDischargeSlots | None = None
310+
max_output_power: InverterMaxOutputPower | None = None
305311
max_export_power: InverterMaxExportPower | None = None
306312
power_limit: InverterPowerLimit | None = None
307313
battery_reserve_soc: InverterBatteryReserveSOC | None = None
@@ -332,6 +338,7 @@ def create_hybrid_inverter(
332338
storage_mode=InverterStorageMode(),
333339
charge_discharge_settings=InverterChargeDischargeSettings(),
334340
charge_discharge_slots=InverterChargeDischargeSlots(),
341+
max_output_power=InverterMaxOutputPower(),
335342
max_export_power=InverterMaxExportPower(),
336343
battery_reserve_soc=InverterBatteryReserveSOC(),
337344
battery_over_discharge_soc=InverterBatteryOverDischargeSOC(),
@@ -355,6 +362,8 @@ def all_cids(self) -> list[int]:
355362
cids.append(self.charge_discharge_settings.cid)
356363
if self.charge_discharge_slots:
357364
cids.extend(self.charge_discharge_slots.all_cids)
365+
if self.max_output_power:
366+
cids.append(self.max_output_power.cid)
358367
if self.max_export_power:
359368
cids.append(self.max_export_power.cid)
360369
if self.power_limit:

custom_components/solis_cloud_control/number.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
InverterBatteryOverDischargeSOC,
1414
InverterChargeDischargeSlot,
1515
InverterMaxExportPower,
16+
InverterMaxOutputPower,
1617
InverterPowerLimit,
1718
)
1819
from custom_components.solis_cloud_control.utils.safe_converters import safe_get_float_value
@@ -84,6 +85,19 @@ async def async_setup_entry(
8485
]
8586
)
8687

88+
if inverter.max_output_power is not None:
89+
entities.append(
90+
MaxOutputPower(
91+
coordinator=coordinator,
92+
entity_description=NumberEntityDescription(
93+
key="max_output_power",
94+
name="Max Output Power",
95+
icon="mdi:lightning-bolt-outline",
96+
),
97+
max_output_power=inverter.max_output_power,
98+
)
99+
)
100+
87101
if inverter.max_export_power is not None:
88102
entities.append(
89103
MaxExportPower(
@@ -210,6 +224,32 @@ async def async_set_native_value(self, value: float) -> None:
210224
await self.coordinator.control(self.charge_discharge_slot.soc_cid, value_str)
211225

212226

227+
class MaxOutputPower(SolisCloudControlEntity, NumberEntity):
228+
def __init__(
229+
self,
230+
coordinator: SolisCloudControlCoordinator,
231+
entity_description: NumberEntityDescription,
232+
max_output_power: InverterMaxOutputPower,
233+
) -> None:
234+
super().__init__(coordinator, entity_description, max_output_power.cid)
235+
self.max_output_power = max_output_power
236+
237+
self._attr_native_min_value = 0
238+
self._attr_native_max_value = 100
239+
self._attr_native_step = 1
240+
self._attr_native_unit_of_measurement = PERCENTAGE
241+
242+
@property
243+
def native_value(self) -> float | None:
244+
value_str = self.coordinator.data.get(self.max_output_power.cid)
245+
return safe_get_float_value(value_str)
246+
247+
async def async_set_native_value(self, value: float) -> None:
248+
value_str = str(int(round(value)))
249+
_LOGGER.info("Setting max output power to %f (value: %s)", value, value_str)
250+
await self.coordinator.control(self.max_output_power.cid, value_str)
251+
252+
213253
class MaxExportPower(SolisCloudControlEntity, NumberEntity):
214254
def __init__(
215255
self,

tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
InverterChargeDischargeSlots,
2020
InverterInfo,
2121
InverterMaxExportPower,
22+
InverterMaxOutputPower,
2223
InverterOnOff,
2324
InverterStorageMode,
2425
)
@@ -93,6 +94,7 @@ def any_inverter(any_inverter_info: InverterInfo) -> Inverter:
9394
storage_mode=InverterStorageMode(),
9495
charge_discharge_settings=InverterChargeDischargeSettings(),
9596
charge_discharge_slots=InverterChargeDischargeSlots(),
97+
max_output_power=InverterMaxOutputPower(),
9698
max_export_power=InverterMaxExportPower(),
9799
power_limit=InverterPowerLimit(),
98100
battery_reserve_soc=InverterBatteryReserveSOC(),

tests/test_init.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ async def test_async_setup_entry(hass, mock_api_client, mock_config_entry, any_i
5959
entries = er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)
6060

6161
platform_counts = Counter(entry.domain for entry in entries)
62-
assert platform_counts[Platform.NUMBER] == 26
62+
assert platform_counts[Platform.NUMBER] == 27
6363
assert platform_counts[Platform.SELECT] == 1
6464
assert platform_counts[Platform.SENSOR] == 7
6565
assert platform_counts[Platform.SWITCH] == 15

tests/test_number.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
BatteryCurrent,
99
BatterySoc,
1010
MaxExportPower,
11+
MaxOutputPower,
1112
PowerLimit,
1213
)
1314

@@ -127,6 +128,54 @@ async def test_set_native_value(self, battery_soc_entity, value, expected_str):
127128
)
128129

129130

131+
@pytest.fixture
132+
def max_output_power_entity(mock_coordinator, any_inverter):
133+
return MaxOutputPower(
134+
coordinator=mock_coordinator,
135+
entity_description=NumberEntityDescription(
136+
key="any_key",
137+
name="any name",
138+
),
139+
max_output_power=any_inverter.max_output_power,
140+
)
141+
142+
143+
class TestMaxOutputPower:
144+
def test_attributes(self, max_output_power_entity):
145+
assert max_output_power_entity.native_min_value == 0
146+
assert max_output_power_entity.native_max_value == 100
147+
assert max_output_power_entity.native_step == 1
148+
assert max_output_power_entity.native_unit_of_measurement == PERCENTAGE
149+
150+
@pytest.mark.parametrize(
151+
("value", "expected"),
152+
[
153+
("0", 0.0),
154+
("50", 50.0),
155+
("100", 100.0),
156+
("not a number", None),
157+
(None, None),
158+
],
159+
)
160+
def test_native_value(self, max_output_power_entity, value, expected):
161+
max_output_power_entity.coordinator.data = {max_output_power_entity.max_output_power.cid: value}
162+
assert max_output_power_entity.native_value == expected
163+
164+
@pytest.mark.parametrize(
165+
("value", "expected_str"),
166+
[
167+
(0.1, "0"),
168+
(50.4, "50"),
169+
(99.9, "100"),
170+
],
171+
)
172+
async def test_set_native_value(self, max_output_power_entity, value, expected_str):
173+
await max_output_power_entity.async_set_native_value(value)
174+
max_output_power_entity.coordinator.control.assert_awaited_once_with(
175+
max_output_power_entity.max_output_power.cid, expected_str
176+
)
177+
178+
130179
@pytest.fixture
131180
def max_export_power_entity(mock_coordinator, any_inverter):
132181
return MaxExportPower(

0 commit comments

Comments
 (0)