From 00e522c0aa110212e9ff384e9cc8dca03c7b9d37 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sat, 16 Nov 2024 16:51:57 +0000 Subject: [PATCH 1/2] issue #76 - device without offpeak_time should not starts at midnight --- .../solar_optimizer/managed_device.py | 32 +++-- tests/test_min_in_time.py | 131 +++++++++++++++++- 2 files changed, 147 insertions(+), 16 deletions(-) diff --git a/custom_components/solar_optimizer/managed_device.py b/custom_components/solar_optimizer/managed_device.py index 4c49988..109fa58 100644 --- a/custom_components/solar_optimizer/managed_device.py +++ b/custom_components/solar_optimizer/managed_device.py @@ -193,9 +193,13 @@ def __init__(self, hass: HomeAssistant, device_config, coordinator): int(device_config.get("min_on_time_per_day_min") or 0) * 60 ) - self._offpeak_time = datetime.strptime( - device_config.get("offpeak_time") or "23:59", "%H:%M" - ).time() + offpeak_time = device_config.get("offpeak_time", None) + self._offpeak_time = None + + if offpeak_time: + self._offpeak_time = datetime.strptime( + device_config.get("offpeak_time"), "%H:%M" + ).time() if self.is_active: self._requested_power = self._current_power = ( @@ -206,7 +210,7 @@ def __init__(self, hass: HomeAssistant, device_config, coordinator): # Some checks # min_on_time_per_day_sec requires an offpeak_time - if self._min_on_time_per_day_sec > 0 and self._offpeak_time == time(23, 59): + if self._min_on_time_per_day_sec > 0 and self._offpeak_time is None: msg = f"configuration of device ${self.name} is incorrect. min_on_time_per_day_sec requires offpeak_time value" _LOGGER.error("%s - %s", self, msg) raise ConfigurationError(msg) @@ -373,12 +377,8 @@ def is_active(self) -> bool: return result - @property - def is_usable(self) -> bool: - """A device is usable for optimisation if the check_usable_template returns true and - if the device is not waiting for the end of its cycle and if the battery_soc_threshold is >= battery_soc - and the _max_on_time_per_day_sec is not exceeded""" - + def check_usable(self, check_battery=True) -> bool: + """Check if the device is usable. The battery is checked optionally""" if self._on_time_sec >= self._max_on_time_per_day_sec: _LOGGER.debug( "%s is not usable due to max_on_time_per_day_min exceeded %d >= %d", @@ -398,7 +398,8 @@ def is_usable(self) -> bool: _LOGGER.debug("%s is not usable", self._name) if ( - result + check_battery + and result and self._battery_soc is not None and self._battery_soc_threshold is not None ): @@ -413,10 +414,17 @@ def is_usable(self) -> bool: return result + @property + def is_usable(self) -> bool: + """A device is usable for optimisation if the check_usable_template returns true and + if the device is not waiting for the end of its cycle and if the battery_soc_threshold is >= battery_soc + and the _max_on_time_per_day_sec is not exceeded""" + return self.check_usable(True) + @property def should_be_forced_offpeak(self) -> bool: """True is we are offpeak and the max_on_time is not exceeded""" - if not self.is_usable: + if not self.check_usable(False) or self._offpeak_time is None: return False if self._offpeak_time >= self._coordinator.raz_time: diff --git a/tests/test_min_in_time.py b/tests/test_min_in_time.py index 612df36..51f8c6e 100644 --- a/tests/test_min_in_time.py +++ b/tests/test_min_in_time.py @@ -182,6 +182,8 @@ async def test_min_on_time_config_ko_3( assert device.offpeak_time == time(1, 0, 0) device._set_now(current_datetime.replace(tzinfo=get_tz(hass))) + # 5 minutes back so that device is_available + device._next_date_available = device.now - timedelta(minutes=5) with patch( "custom_components.solar_optimizer.managed_device.ManagedDevice.is_usable", @@ -276,21 +278,21 @@ async def test_nominal_use_min_on_time( "custom_components.solar_optimizer.managed_device.ManagedDevice.is_usable", return_value=True, ): - now = datetime(2024, 11, 10, 1, 00, 00).replace(tzinfo=get_tz(hass)) + now = datetime(2024, 11, 11, 1, 00, 00).replace(tzinfo=get_tz(hass)) device_a._set_now(now) assert device_a.should_be_forced_offpeak is True # # 6. at 04:59 it should be possible to force offpeak # - now = datetime(2024, 11, 10, 4, 59, 00).replace(tzinfo=get_tz(hass)) + now = datetime(2024, 11, 11, 4, 59, 00).replace(tzinfo=get_tz(hass)) device_a._set_now(now) assert device_a.should_be_forced_offpeak is True # # 6. at 05:01 it should be not possible to force offpeak # - now = datetime(2024, 11, 10, 5, 1, 00).replace(tzinfo=get_tz(hass)) + now = datetime(2024, 11, 11, 5, 1, 00).replace(tzinfo=get_tz(hass)) device_a._set_now(now) assert device_a.should_be_forced_offpeak is False @@ -298,7 +300,7 @@ async def test_nominal_use_min_on_time( # 7. when on_time is > max_on_time it should be not possible to force off_peak # # Come back in offpeak - now = datetime(2024, 11, 10, 0, 0, 00).replace(tzinfo=get_tz(hass)) + now = datetime(2024, 11, 11, 0, 0, 00).replace(tzinfo=get_tz(hass)) device_a._set_now(now) assert device_a.should_be_forced_offpeak is True @@ -314,3 +316,124 @@ async def test_nominal_use_min_on_time( assert device_a._on_time_sec == 10 * 60 assert device_a.should_be_forced_offpeak is False + + +async def test_nominal_use_offpeak_without_min( + hass: HomeAssistant, +): + """Testing the nominal case with min_on_time and offpeak_time""" + + await async_setup_component( + hass, + "solar_optimizer", + { + "solar_optimizer": { + "algorithm": { + "initial_temp": 1000, + "min_temp": 0.1, + "cooling_factor": 0.95, + "max_iteration_number": 1000, + }, + "devices": [ + { + "name": "Equipement A", + "entity_id": "input_boolean.fake_device_a", + "power_max": 1000, + "check_usable_template": "{{ True }}", + "duration_min": 2, + "duration_stop_min": 1, + "action_mode": "service_call", + "activation_service": "input_boolean/turn_on", + "deactivation_service": "input_boolean/turn_off", + "battery_soc_threshold": 30, + "max_on_time_per_day_min": 10, + } + ], + } + }, + ) + coordinator: SolarOptimizerCoordinator = hass.data[DOMAIN]["coordinator"] + + assert coordinator is not None + assert coordinator.devices is not None + assert len(coordinator.devices) == 1 + + entry = MockConfigEntry( + domain=DOMAIN, + title="TheSolarOptimizer", + unique_id="uniqueId", + data={}, + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + + # default value + assert coordinator.raz_time == time(5, 0, 0) + + # 0. check default values + device_a: ManagedDevice = coordinator.devices[0] + assert device_a.min_on_time_per_day_sec == 0 + # default offpeak_time + assert device_a.offpeak_time is None + + now = datetime(2024, 11, 10, 10, 00, 00).replace(tzinfo=get_tz(hass)) + device_a._set_now(now) + + # Creates the fake input_boolean (the device) + await create_test_input_boolean(hass, device_a.entity_id, "fake underlying A") + fake_input_bool = search_entity( + hass, "input_boolean.fake_device_a", INPUT_BOOLEAN_DOMAIN + ) + assert fake_input_bool is not None + + # The on_time should be 0 + device_a_on_time_sensor = search_entity( + hass, "sensor.on_time_today_solar_optimizer_equipement_a", SENSOR_DOMAIN + ) + assert device_a_on_time_sensor.state == 0 + assert device_a_on_time_sensor.last_datetime_on is None + device_a.reset_next_date_available( + "Activate" + ) # for test only to reset the next_available date + + # + # 2. Activate the underlying switch during 3 minutes + # + assert device_a.is_usable is False # because duration_min is 3 minutes + assert device_a.is_enabled is True + + await fake_input_bool.async_turn_on() + await hass.async_block_till_done() + now = now + timedelta(minutes=4) + device_a._set_now(now) + + assert device_a.is_usable is True + # before 23:00 device should not be forcable + assert device_a.should_be_forced_offpeak is False + + await fake_input_bool.async_turn_off() + await hass.async_block_till_done() + + assert device_a_on_time_sensor.state == 4 * 60 + + # + # 4. just at 23:59 it should be not possible to force offpeak + # + now = datetime(2024, 11, 10, 23, 59, 00).replace(tzinfo=get_tz(hass)) + device_a._set_now(now) + assert device_a.should_be_forced_offpeak is False + + # + # 5. at 01:00 it should be not possible to force offpeak + # + with patch( + "custom_components.solar_optimizer.managed_device.ManagedDevice.is_usable", + return_value=True, + ): + now = datetime(2024, 11, 10, 1, 00, 00).replace(tzinfo=get_tz(hass)) + device_a._set_now(now) + assert device_a.should_be_forced_offpeak is False From 8eac78a31aa28e95347bcc4702181d3feebd35c5 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sat, 16 Nov 2024 16:53:52 +0000 Subject: [PATCH 2/2] release --- custom_components/solar_optimizer/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/solar_optimizer/manifest.json b/custom_components/solar_optimizer/manifest.json index da41c54..e25558d 100644 --- a/custom_components/solar_optimizer/manifest.json +++ b/custom_components/solar_optimizer/manifest.json @@ -10,5 +10,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/jmcollin78/solar_optimizer/issues", "quality_scale": "silver", - "version": "2.1.0" + "version": "2.1.1" } \ No newline at end of file