diff --git a/README.md b/README.md index c766060..09cf7a8 100644 --- a/README.md +++ b/README.md @@ -78,14 +78,13 @@ All Solis inverters should be supported, although the integration has been teste The integration provides a user-friendly interface to control your inverter settings. It allows you to: -- ⚡ Control storage modes: "Self-Use", "Feed-In Priority" and "Off-Grid" 🟢 +- ⚡ Control storage modes: "Self-Use", "Feed-In Priority", "Off-Grid" and "Grid Peak-Shaving" 🟢 - ⏱️ Schedule charge and discharge slots 🟢 - Switch the inverter on or off 🟢 ⚪️ - Set the inverter time 🟢 ⚪️ - Toggle "Battery Reserve" 🟢 - Toggle "Allow Grid Charging" 🟢 - Toggle "Allow Export" (in "Self-Use" storage mode) 🟢 -- Toggle "Grid Peak Shaving" (in "Self-Use" and "Feed-In Priority" storage modes) 🟢 - Set maximum output power 🟢 - Set maximum export power 🟢 - Control various Battery State of Charge (SOC) levels 🟢 diff --git a/custom_components/solis_cloud_control/domain/storage_mode.py b/custom_components/solis_cloud_control/domain/storage_mode.py index 8741c62..bb352e2 100644 --- a/custom_components/solis_cloud_control/domain/storage_mode.py +++ b/custom_components/solis_cloud_control/domain/storage_mode.py @@ -27,15 +27,15 @@ def is_feed_in_priority(self) -> bool: def is_off_grid(self) -> bool: return (self.mode & (1 << self.BIT_OFF_GRID)) != 0 + def is_peak_shaving(self) -> bool: + return (self.mode & (1 << self.BIT_PEAK_SHAVING)) != 0 + def is_battery_reserve_enabled(self) -> bool: return (self.mode & (1 << self.BIT_BACKUP_MODE)) != 0 def is_allow_grid_charging(self) -> bool: return (self.mode & (1 << self.BIT_GRID_CHARGING)) != 0 - def is_peak_shaving(self) -> bool: - return (self.mode & (1 << self.BIT_PEAK_SHAVING)) != 0 - def is_tou_mode(self) -> bool: return (self.mode & (1 << self.BIT_TOU_MODE)) != 0 @@ -43,16 +43,25 @@ def set_self_use(self) -> None: self.mode |= 1 << self.BIT_SELF_USE self.mode &= ~(1 << self.BIT_FEED_IN_PRIORITY) self.mode &= ~(1 << self.BIT_OFF_GRID) + self.mode &= ~(1 << self.BIT_PEAK_SHAVING) def set_feed_in_priority(self) -> None: self.mode |= 1 << self.BIT_FEED_IN_PRIORITY self.mode &= ~(1 << self.BIT_SELF_USE) self.mode &= ~(1 << self.BIT_OFF_GRID) + self.mode &= ~(1 << self.BIT_PEAK_SHAVING) def set_off_grid(self) -> None: self.mode |= 1 << self.BIT_OFF_GRID self.mode &= ~(1 << self.BIT_SELF_USE) self.mode &= ~(1 << self.BIT_FEED_IN_PRIORITY) + self.mode &= ~(1 << self.BIT_PEAK_SHAVING) + + def set_peak_shaving(self) -> None: + self.mode |= 1 << self.BIT_PEAK_SHAVING + self.mode &= ~(1 << self.BIT_SELF_USE) + self.mode &= ~(1 << self.BIT_FEED_IN_PRIORITY) + self.mode &= ~(1 << self.BIT_OFF_GRID) def enable_battery_reserve(self) -> None: self.mode |= 1 << self.BIT_BACKUP_MODE @@ -66,12 +75,6 @@ def enable_allow_grid_charging(self) -> None: def disable_allow_grid_charging(self) -> None: self.mode &= ~(1 << self.BIT_GRID_CHARGING) - def enable_peak_shaving(self) -> None: - self.mode |= 1 << self.BIT_PEAK_SHAVING - - def disable_peak_shaving(self) -> None: - self.mode &= ~(1 << self.BIT_PEAK_SHAVING) - def enable_tou_mode(self) -> None: self.mode |= 1 << self.BIT_TOU_MODE diff --git a/custom_components/solis_cloud_control/select.py b/custom_components/solis_cloud_control/select.py index 73fcdff..fe3ae1f 100644 --- a/custom_components/solis_cloud_control/select.py +++ b/custom_components/solis_cloud_control/select.py @@ -44,6 +44,7 @@ class StorageModeSelect(SolisCloudControlEntity, SelectEntity): MODE_SELF_USE: str = "Self-Use" MODE_FEED_IN_PRIORITY: str = "Feed-In Priority" MODE_OFF_GRID: str = "Off-Grid" + MODE_GRID_PEAK_SHAVING: str = "Grid Peak-Shaving" def __init__( self, @@ -56,6 +57,7 @@ def __init__( self.MODE_SELF_USE, self.MODE_FEED_IN_PRIORITY, self.MODE_OFF_GRID, + self.MODE_GRID_PEAK_SHAVING, ] self.inverter_storage_mode = inverter_storage_mode @@ -75,6 +77,8 @@ def current_option(self) -> str | None: return self.MODE_FEED_IN_PRIORITY elif storage_mode.is_off_grid(): return self.MODE_OFF_GRID + elif storage_mode.is_peak_shaving(): + return self.MODE_GRID_PEAK_SHAVING return None @@ -92,6 +96,8 @@ async def async_select_option(self, option: str) -> None: storage_mode.set_feed_in_priority() elif option == self.MODE_OFF_GRID: storage_mode.set_off_grid() + elif option == self.MODE_GRID_PEAK_SHAVING: + storage_mode.set_peak_shaving() value_str = storage_mode.to_value() diff --git a/custom_components/solis_cloud_control/switch.py b/custom_components/solis_cloud_control/switch.py index 8493195..bd126f7 100644 --- a/custom_components/solis_cloud_control/switch.py +++ b/custom_components/solis_cloud_control/switch.py @@ -109,17 +109,6 @@ async def async_setup_entry( inverter_storage_mode=inverter.storage_mode, ) ) - entities.append( - GridPeakShavingSwitch( - coordinator=coordinator, - entity_description=SwitchEntityDescription( - key="grid_peak_shaving", - name="Grid Peak Shaving", - icon="mdi:chart-bell-curve", - ), - inverter_storage_mode=inverter.storage_mode, - ) - ) if inverter.storage_mode is not None and not inverter.info.is_tou_v2_enabled: entities.append( @@ -403,71 +392,6 @@ async def async_turn_off(self, **kwargs) -> None: # noqa: ANN003, ARG002 await self.coordinator.control(self.inverter_storage_mode.cid, value_str) -class GridPeakShavingSwitch(SolisCloudControlEntity, SwitchEntity): - def __init__( - self, - coordinator: SolisCloudControlCoordinator, - entity_description: SwitchEntityDescription, - inverter_storage_mode: InverterStorageMode, - ) -> None: - super().__init__(coordinator, entity_description, inverter_storage_mode.cid) - self.inverter_storage_mode = inverter_storage_mode - - @property - def is_on(self) -> bool | None: - current_value = self.coordinator.data.get(self.inverter_storage_mode.cid) - - storage_mode = StorageMode.create(current_value) - if storage_mode is None: - _LOGGER.warning("Invalid '%s' storage mode: '%s'", self.name, current_value) - return None - - return storage_mode.is_peak_shaving() - - @property - def available(self) -> bool: - if not super().available: - return False - - storage_mode_value = self.coordinator.data.get(self.inverter_storage_mode.cid) - storage_mode = StorageMode.create(storage_mode_value) - if storage_mode is None: - _LOGGER.warning("Invalid '%s' storage mode: '%s'", self.name, storage_mode_value) - return False - - return storage_mode.is_self_use() or storage_mode.is_feed_in_priority() - - async def async_turn_on(self, **kwargs) -> None: # noqa: ANN003, ARG002 - current_value = self.coordinator.data.get(self.inverter_storage_mode.cid) - - storage_mode = StorageMode.create(current_value) - if storage_mode is None: - _LOGGER.warning("Invalid '%s' storage mode: '%s'", self.name, current_value) - return None - - storage_mode.enable_peak_shaving() - - value_str = storage_mode.to_value() - - _LOGGER.info("Turn on '%s' (value: %s)", self.name, value_str) - await self.coordinator.control(self.inverter_storage_mode.cid, value_str) - - async def async_turn_off(self, **kwargs) -> None: # noqa: ANN003, ARG002 - current_value = self.coordinator.data.get(self.inverter_storage_mode.cid) - - storage_mode = StorageMode.create(current_value) - if storage_mode is None: - _LOGGER.warning("Invalid '%s' storage mode: '%s'", self.name, current_value) - return None - - storage_mode.disable_peak_shaving() - - value_str = storage_mode.to_value() - - _LOGGER.info("Turn off '%s' (value: %s)", self.name, value_str) - await self.coordinator.control(self.inverter_storage_mode.cid, value_str) - - class TimeOfUseSwitch(SolisCloudControlEntity, SwitchEntity): def __init__( self, diff --git a/tests/domain/test_storage_mode.py b/tests/domain/test_storage_mode.py index 3cdf718..6e3b57b 100644 --- a/tests/domain/test_storage_mode.py +++ b/tests/domain/test_storage_mode.py @@ -37,6 +37,11 @@ def test_is_off_grid(self): assert storage_mode.is_off_grid() is True + def test_is_peak_shaving(self): + storage_mode = StorageMode(2048) + + assert storage_mode.is_peak_shaving() is True + def test_is_battery_reserve_enabled(self): storage_mode = StorageMode(16) @@ -47,11 +52,6 @@ def test_is_allow_grid_charging(self): assert storage_mode.is_allow_grid_charging() is True - def test_is_peak_shaving(self): - storage_mode = StorageMode(2048) - - assert storage_mode.is_peak_shaving() is True - def test_is_tou_mode(self): storage_mode = StorageMode(2) @@ -64,6 +64,7 @@ def test_set_self_use(self, storage_mode): assert storage_mode.is_self_use() is True assert storage_mode.is_feed_in_priority() is False assert storage_mode.is_off_grid() is False + assert storage_mode.is_peak_shaving() is False @pytest.mark.parametrize("storage_mode", [STORAGE_MODE_0x0000, STORAGE_MODE_0xFFFF]) def test_set_feed_in_priority(self, storage_mode): @@ -72,6 +73,7 @@ def test_set_feed_in_priority(self, storage_mode): assert storage_mode.is_feed_in_priority() is True assert storage_mode.is_self_use() is False assert storage_mode.is_off_grid() is False + assert storage_mode.is_peak_shaving() is False @pytest.mark.parametrize("storage_mode", [STORAGE_MODE_0x0000, STORAGE_MODE_0xFFFF]) def test_set_off_grid(self, storage_mode): @@ -80,6 +82,16 @@ def test_set_off_grid(self, storage_mode): assert storage_mode.is_off_grid() is True assert storage_mode.is_self_use() is False assert storage_mode.is_feed_in_priority() is False + assert storage_mode.is_peak_shaving() is False + + @pytest.mark.parametrize("storage_mode", [STORAGE_MODE_0x0000, STORAGE_MODE_0xFFFF]) + def test_set_peak_shaving(self, storage_mode): + storage_mode.set_peak_shaving() + + assert storage_mode.is_peak_shaving() is True + assert storage_mode.is_self_use() is False + assert storage_mode.is_feed_in_priority() is False + assert storage_mode.is_off_grid() is False @pytest.mark.parametrize("storage_mode", [STORAGE_MODE_0x0000, STORAGE_MODE_0xFFFF]) def test_enable_battery_reserve(self, storage_mode): @@ -105,18 +117,6 @@ def test_disable_allow_grid_charging(self, storage_mode): assert storage_mode.is_allow_grid_charging() is False - @pytest.mark.parametrize("storage_mode", [STORAGE_MODE_0x0000, STORAGE_MODE_0xFFFF]) - def test_enable_peak_shaving(self, storage_mode): - storage_mode.enable_peak_shaving() - - assert storage_mode.is_peak_shaving() is True - - @pytest.mark.parametrize("storage_mode", [STORAGE_MODE_0x0000, STORAGE_MODE_0xFFFF]) - def test_disable_peak_shaving(self, storage_mode): - storage_mode.disable_peak_shaving() - - assert storage_mode.is_peak_shaving() is False - @pytest.mark.parametrize("storage_mode", [STORAGE_MODE_0x0000, STORAGE_MODE_0xFFFF]) def test_enable_tou_mode(self, storage_mode): storage_mode.enable_tou_mode() diff --git a/tests/test_init.py b/tests/test_init.py index 5ac1c9b..14063eb 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -90,7 +90,7 @@ async def test_async_setup_entry(hass, mock_api_client, mock_config_entry, any_i assert platform_counts[Platform.NUMBER] == 40 assert platform_counts[Platform.SELECT] == 1 assert platform_counts[Platform.SENSOR] == 7 - assert platform_counts[Platform.SWITCH] == 18 + assert platform_counts[Platform.SWITCH] == 17 assert platform_counts[Platform.TEXT] == 18 platform_disabled_counts = Counter(entry.domain for entry in entries if entry.disabled_by is not None) diff --git a/tests/test_select.py b/tests/test_select.py index a9083f4..8d91177 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -20,6 +20,7 @@ async def test_options(self, storage_mode_entity): storage_mode_entity.MODE_SELF_USE, storage_mode_entity.MODE_FEED_IN_PRIORITY, storage_mode_entity.MODE_OFF_GRID, + storage_mode_entity.MODE_GRID_PEAK_SHAVING, ] @pytest.mark.parametrize( @@ -28,6 +29,7 @@ async def test_options(self, storage_mode_entity): (str(1 << StorageMode.BIT_SELF_USE), StorageModeSelect.MODE_SELF_USE), (str(1 << StorageMode.BIT_FEED_IN_PRIORITY), StorageModeSelect.MODE_FEED_IN_PRIORITY), (str(1 << StorageMode.BIT_OFF_GRID), StorageModeSelect.MODE_OFF_GRID), + (str(1 << StorageMode.BIT_PEAK_SHAVING), StorageModeSelect.MODE_GRID_PEAK_SHAVING), (str(0), None), ("not a number", None), (None, None), @@ -43,6 +45,7 @@ async def test_current_option(self, storage_mode_entity, value, expected_mode): (StorageModeSelect.MODE_SELF_USE, str(1 << StorageMode.BIT_SELF_USE)), (StorageModeSelect.MODE_FEED_IN_PRIORITY, str(1 << StorageMode.BIT_FEED_IN_PRIORITY)), (StorageModeSelect.MODE_OFF_GRID, str(1 << StorageMode.BIT_OFF_GRID)), + (StorageModeSelect.MODE_GRID_PEAK_SHAVING, str(1 << StorageMode.BIT_PEAK_SHAVING)), ], ) async def test_async_select_option(self, storage_mode_entity, mode, expected_value): diff --git a/tests/test_switch.py b/tests/test_switch.py index 255d9ad..ae89aea 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -6,7 +6,6 @@ AllowExportSwitch, AllowGridChargingSwitch, BatteryReserveSwitch, - GridPeakShavingSwitch, OnOffSwitch, SlotV2Switch, TimeOfUseSwitch, @@ -389,83 +388,3 @@ async def test_async_turn_on_off_invalid_initial(self, time_of_use_switch, initi await time_of_use_switch.async_turn_on() await time_of_use_switch.async_turn_off() time_of_use_switch.coordinator.control.assert_not_awaited() - - -@pytest.fixture -def grid_peak_shaving_switch(mock_coordinator, any_inverter): - return GridPeakShavingSwitch( - coordinator=mock_coordinator, - entity_description=SwitchEntityDescription(key="any_key", name="any name"), - inverter_storage_mode=any_inverter.storage_mode, - ) - - -class TestGridPeakShavingSwitch: - def test_is_on_when_none(self, grid_peak_shaving_switch): - grid_peak_shaving_switch.coordinator.data = {grid_peak_shaving_switch.inverter_storage_mode.cid: None} - assert grid_peak_shaving_switch.is_on is None - - def test_is_on_when_on(self, grid_peak_shaving_switch): - bit = StorageMode.BIT_PEAK_SHAVING - grid_peak_shaving_switch.coordinator.data = {grid_peak_shaving_switch.inverter_storage_mode.cid: str(1 << bit)} - assert grid_peak_shaving_switch.is_on is True - - def test_is_on_when_off(self, grid_peak_shaving_switch): - grid_peak_shaving_switch.coordinator.data = {grid_peak_shaving_switch.inverter_storage_mode.cid: str(0)} - assert grid_peak_shaving_switch.is_on is False - - def test_available_when_storage_mode_is_self_use(self, grid_peak_shaving_switch): - mode = str(1 << StorageMode.BIT_SELF_USE) - grid_peak_shaving_switch.coordinator.data = { - grid_peak_shaving_switch.inverter_storage_mode.cid: mode, - } - assert grid_peak_shaving_switch.available is True - - def test_available_when_storage_mode_is_feed_in_priority(self, grid_peak_shaving_switch): - mode = str(1 << StorageMode.BIT_FEED_IN_PRIORITY) - grid_peak_shaving_switch.coordinator.data = { - grid_peak_shaving_switch.inverter_storage_mode.cid: mode, - } - assert grid_peak_shaving_switch.available is True - - def test_available_when_storage_mode_is_off_grid(self, grid_peak_shaving_switch): - mode = str(1 << StorageMode.BIT_OFF_GRID) - grid_peak_shaving_switch.coordinator.data = { - grid_peak_shaving_switch.inverter_storage_mode.cid: mode, - } - assert grid_peak_shaving_switch.available is False - - def test_available_when_storage_mode_none(self, grid_peak_shaving_switch): - grid_peak_shaving_switch.coordinator.data = { - grid_peak_shaving_switch.inverter_storage_mode.cid: None, - } - assert grid_peak_shaving_switch.available is False - - async def test_turn_on(self, grid_peak_shaving_switch): - bit = StorageMode.BIT_PEAK_SHAVING - grid_peak_shaving_switch.coordinator.data = {grid_peak_shaving_switch.inverter_storage_mode.cid: str(0)} - await grid_peak_shaving_switch.async_turn_on() - grid_peak_shaving_switch.coordinator.control.assert_awaited_once_with( - grid_peak_shaving_switch.inverter_storage_mode.cid, str(1 << bit) - ) - - async def test_turn_off(self, grid_peak_shaving_switch): - bit = StorageMode.BIT_PEAK_SHAVING - grid_peak_shaving_switch.coordinator.data = {grid_peak_shaving_switch.inverter_storage_mode.cid: str(1 << bit)} - await grid_peak_shaving_switch.async_turn_off() - grid_peak_shaving_switch.coordinator.control.assert_awaited_once_with( - grid_peak_shaving_switch.inverter_storage_mode.cid, str(0) - ) - - @pytest.mark.parametrize( - "initial_value", - [ - "not a number", - None, - ], - ) - async def test_async_turn_on_off_invalid_initial(self, grid_peak_shaving_switch, initial_value): - grid_peak_shaving_switch.coordinator.data = {grid_peak_shaving_switch.inverter_storage_mode.cid: initial_value} - await grid_peak_shaving_switch.async_turn_on() - await grid_peak_shaving_switch.async_turn_off() - grid_peak_shaving_switch.coordinator.control.assert_not_awaited()