Skip to content

Commit 850e29a

Browse files
committed
feat: added configuration for standard target timeframes to determine minimum minutes for a slot to be evaluated (1 hour 30 minutes dev time)
1 parent a61edfa commit 850e29a

File tree

10 files changed

+391
-289
lines changed

10 files changed

+391
-289
lines changed

_docs/setup/target_timeframe.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ These settings can have undesired effects and are not recommended to be changed,
131131

132132
By default, the target timeframe isn't calculated if there isn't enough data for the period of time being evaluated. For example, if you have a timeframe looking between 10pm and 2am, it's 9pm and you only have data up to midnight, then the next target timeframe will not be calculated. If you turn this setting on, then the sensor will attempt to look for data between 10pm and 2am if available, otherwise it will evaluate with whatever data is available (in this scenario 10pm to 12am).
133133

134+
#### Minimum required minutes in slots
135+
136+
By default, 30 minute slots that are part way through are not considered when evaluating target time frames. For example, if you are looking for the best slots between 10:00 to 12:00 and it's 10:01, then only slots between 10:30 to 12:00 will be evaluated. This threshold can be changed here to a lower value if you want to take account of slots that are partially in the past. For example if this was set to 29, then the previous example would evaluate slots between 10:00 to 12:00.
137+
138+
!!! warn
139+
140+
Changing this can cause sensors to not come on for the correct amount of time by up to 30 minutes.
141+
134142
## Attributes
135143

136144
The following attributes are available on each sensor
@@ -163,6 +171,7 @@ The following attributes are available on each sensor
163171
| `next_max_value` | `float` | The average value for the next continuous discovered period. This will only be populated if `target_times` has been calculated and at least one period/block is in the future. |
164172
| `target_times_last_evaluated` | datetime | The datetime the target times collection was last evaluated. This will occur if all previous target times are in the past and all values are available for the requested future time period. For example, if you are targeting 16:00 (day 1) to 16:00 (day 2), and you only have values up to 23:00 (day 1), then the target values won't be calculated. |
165173
| `calculate_with_incomplete_data` | boolean | Determines if calculations should occur when there isn't enough data to satisfy the look ahead hours |
174+
| `minimum_required_minutes_in_slot` | integer | Determines the configured minimum number of minutes to be present in a slot for it to be considered |
166175

167176
## Services
168177

custom_components/target_timeframes/config/target_timeframe.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
CONFIG_TARGET_HOURS_MODE_MINIMUM,
1212
CONFIG_TARGET_MAX_VALUE,
1313
CONFIG_TARGET_MIN_VALUE,
14+
CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT,
1415
CONFIG_TARGET_NAME,
1516
CONFIG_TARGET_OFFSET,
1617
CONFIG_TARGET_START_TIME,
@@ -162,4 +163,19 @@ def validate_target_timeframe_config(data):
162163
if is_time_frame_long_enough(data[CONFIG_TARGET_HOURS], start_time, end_time) == False:
163164
errors[CONFIG_TARGET_HOURS] = "invalid_hours_time_frame"
164165

166+
minimum_required_minutes_in_slot: int | None = None
167+
if CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT in data and data[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] is not None:
168+
if isinstance(data[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT], int) == False:
169+
matches = re.search(REGEX_VALUE, data[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT])
170+
if matches is None:
171+
errors[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] = "invalid_integer"
172+
else:
173+
minimum_required_minutes_in_slot = float(data[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT])
174+
data[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] = minimum_required_minutes_in_slot
175+
else:
176+
minimum_required_minutes_in_slot = data[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT]
177+
178+
if minimum_required_minutes_in_slot is not None and (minimum_required_minutes_in_slot < 1 or minimum_required_minutes_in_slot > 30):
179+
errors[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] = "invalid_minimum_required_minutes_in_slot"
180+
165181
return errors

custom_components/target_timeframes/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS = "always"
4141
CONFIG_TARGET_DANGEROUS_SETTINGS = "dangerous_settings"
4242
CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA = "calculate_with_incomplete_data"
43+
CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT = "minimum_required_minutes_in_slot"
44+
CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT = 30
4345

4446
CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD = "look_ahead_hours"
4547

@@ -123,6 +125,7 @@
123125
vol.Schema(
124126
{
125127
vol.Required(CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA, default=False): bool,
128+
vol.Required(CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT, default=CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT): int,
126129
}
127130
),
128131
{"collapsed": True},

custom_components/target_timeframes/entities/__init__.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def is_target_timeframe_complete_in_period(current_date: datetime, start_time: d
4343
target_timeframes[-1]["end"] <= current_date
4444
)
4545

46-
def get_start_and_end_times(current_date: datetime, target_start_time: str, target_end_time: str, start_time_not_in_past = True, context: str = None):
46+
def get_start_and_end_times(current_date: datetime, target_start_time: str, target_end_time: str, minimum_slot_minutes = None, context: str = None):
4747
if (target_start_time is not None):
4848
target_start = parse_datetime(current_date.strftime(f"%Y-%m-%dT{target_start_time}:00%z"))
4949
else:
@@ -64,10 +64,15 @@ def get_start_and_end_times(current_date: datetime, target_start_time: str, targ
6464
else:
6565
target_end = target_end + timedelta(days=1)
6666

67-
# If our start date has passed, reset it to current_date to avoid picking a slot in the past
68-
if (start_time_not_in_past == True and target_start < current_date and current_date < target_end):
69-
_LOGGER.debug(f'{context} - Rolling target and {target_start} is in the past. Setting start to {current_date}')
70-
target_start = current_date
67+
if (minimum_slot_minutes is not None and target_start < current_date and current_date < target_end):
68+
current_date_start = current_date.replace(minute=30 if current_date.minute >= 30 else 0, second=0, microsecond=0)
69+
minutes_remaining_in_current_slot = 30 - ((current_date.replace(second=0, microsecond=0) - current_date_start).total_seconds() / 60)
70+
if (minutes_remaining_in_current_slot >= minimum_slot_minutes):
71+
_LOGGER.debug(f'{context} - Current slot is sufficient for minimum slot minutes, so using current date start: {current_date_start}')
72+
target_start = current_date_start
73+
else:
74+
target_start = current_date_start + timedelta(minutes=30)
75+
_LOGGER.debug(f'{context} - Current slot is not sufficient for minimum slot minutes, so using next slot start: {target_start}')
7176

7277
# If our start and end are both in the past, then look to the next day
7378
if (target_start < current_date and target_end < current_date):

custom_components/target_timeframes/entities/target_timeframe.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from ..const import (
2323
CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA,
2424
CONFIG_TARGET_DANGEROUS_SETTINGS,
25+
CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT,
26+
CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT,
2527
CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE,
2628
CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST,
2729
CONFIG_TARGET_HOURS_MODE,
@@ -164,7 +166,13 @@ async def async_update(self):
164166
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._config and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in self._config[CONFIG_TARGET_DANGEROUS_SETTINGS]:
165167
calculate_with_incomplete_data = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
166168

167-
target_start, target_end = get_start_and_end_times(current_local_date, start_time, end_time, True, self._config[CONFIG_TARGET_NAME])
169+
target_start, target_end = get_start_and_end_times(
170+
current_local_date,
171+
start_time,
172+
end_time,
173+
self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] if CONFIG_TARGET_DANGEROUS_SETTINGS in self._config and CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT in self._config[CONFIG_TARGET_DANGEROUS_SETTINGS] else CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT,
174+
self._config[CONFIG_TARGET_NAME]
175+
)
168176
applicable_time_periods = get_fixed_applicable_time_periods(
169177
target_start,
170178
target_end,
@@ -174,7 +182,7 @@ async def async_update(self):
174182
)
175183

176184
# Make sure we haven't already completed for the current target timeframe
177-
applicable_target_start, applicable_target_end = get_start_and_end_times(current_local_date, start_time, end_time, False, self._config[CONFIG_TARGET_NAME])
185+
applicable_target_start, applicable_target_end = get_start_and_end_times(current_local_date, start_time, end_time, None, self._config[CONFIG_TARGET_NAME])
178186
is_target_timeframe_complete = is_rolling_target == False and is_target_timeframe_complete_in_period(
179187
current_local_date,
180188
applicable_target_start,
@@ -368,4 +376,10 @@ def update_default_attributes(self):
368376
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._config and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in self._config[CONFIG_TARGET_DANGEROUS_SETTINGS]:
369377
calculate_with_incomplete_data = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
370378
del self._attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]
371-
self._attributes[CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA] = calculate_with_incomplete_data
379+
self._attributes[CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA] = calculate_with_incomplete_data
380+
381+
minimum_required_minutes_in_slot = CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT
382+
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._config and CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT in self._config[CONFIG_TARGET_DANGEROUS_SETTINGS]:
383+
minimum_required_minutes_in_slot = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT]
384+
del self._attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]
385+
self._attributes[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] = minimum_required_minutes_in_slot

custom_components/target_timeframes/translations/en.json

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@
6363
"name": "Dangerous settings",
6464
"description": "These settings can have undesired consequences if changed. Change at your own risk.",
6565
"data": {
66-
"calculate_with_incomplete_data": "Calculate with incomplete data"
66+
"calculate_with_incomplete_data": "Calculate with incomplete data",
67+
"minimum_required_minutes_in_slot": "Minimum required minutes in slot"
68+
},
69+
"data_description": {
70+
"minimum_required_minutes_in_slot": "The minimum number of minutes that must be present in a slot for it to be considered. Changing this can cause chosen slots to be partially in the past and therefore turn on for less than the desired time."
6771
}
6872
}
6973
}
@@ -98,7 +102,11 @@
98102
"name": "Dangerous settings",
99103
"description": "These settings can have undesired consequences if changed. Change at your own risk.",
100104
"data": {
101-
"calculate_with_incomplete_data": "Calculate with incomplete data"
105+
"calculate_with_incomplete_data": "Calculate with incomplete data",
106+
"minimum_required_minutes_in_slot": "Minimum required minutes in slot"
107+
},
108+
"data_description": {
109+
"minimum_required_minutes_in_slot": "The minimum number of minutes that must be present in a slot for it to be considered. Changing this can cause chosen slots to be partially in the past and therefore turn on for less than the desired time."
102110
}
103111
}
104112
}
@@ -117,7 +125,9 @@
117125
"weighting_not_supported_for_type": "Weighting is only supported for continuous target values",
118126
"weighting_not_supported_for_hour_mode": "Weighting is not supported for this hour mode",
119127
"minimum_or_maximum_value_not_specified": "Either minimum and/or maximum value must be specified for minimum hours mode",
120-
"minimum_value_not_less_than_maximum_value": "Minimum value must be less or equal to the maximum value if both are specified"
128+
"minimum_value_not_less_than_maximum_value": "Minimum value must be less or equal to the maximum value if both are specified",
129+
"invalid_integer": "Value must be a number with no decimal places",
130+
"invalid_minimum_required_minutes_in_slot": "Value must be between 1 and 30"
121131
}
122132
},
123133
"rolling_target_time_period": {
@@ -192,7 +202,9 @@
192202
"weighting_not_supported_for_type": "Weighting is only supported for continuous target values",
193203
"weighting_not_supported_for_hour_mode": "Weighting is not supported for this hour mode",
194204
"minimum_or_maximum_value_not_specified": "Either minimum and/or maximum value must be specified for minimum hours mode",
195-
"minimum_value_not_less_than_maximum_value": "Minimum value must be less or equal to the maximum value if both are specified"
205+
"minimum_value_not_less_than_maximum_value": "Minimum value must be less or equal to the maximum value if both are specified",
206+
"invalid_integer": "Value must be a number with no decimal places",
207+
"invalid_minimum_required_minutes_in_slot": "Value must be between 1 and 30"
196208
}
197209
}
198210
},

0 commit comments

Comments
 (0)