Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue #76 - device without offpeak_time should not starts at midnight #77

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion custom_components/solar_optimizer/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
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
Loading