Skip to content

Commit

Permalink
issue #76 - device without offpeak_time should not starts at midnight
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean-Marc Collin committed Nov 16, 2024
1 parent d746284 commit 00e522c
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 16 deletions.
32 changes: 20 additions & 12 deletions custom_components/solar_optimizer/managed_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand All @@ -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)
Expand Down Expand Up @@ -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",
Expand All @@ -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
):
Expand All @@ -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:
Expand Down
131 changes: 127 additions & 4 deletions tests/test_min_in_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -276,29 +278,29 @@ 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

#
# 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

Expand All @@ -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

0 comments on commit 00e522c

Please sign in to comment.