Skip to content

Commit a61edfa

Browse files
committed
feat: added support to evaluate with incomplete data (1 hour dev time)
1 parent d3319e2 commit a61edfa

File tree

9 files changed

+227
-42
lines changed

9 files changed

+227
-42
lines changed

_docs/setup/rolling_target_timeframe.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ Each slot weighting/multiplier must be a whole number or decimal number and be p
113113

114114
You can also use weightings/multipliers to ignore slots. This can be done by assigning a value of 0 for the desired slot.
115115

116+
### Dangerous settings
117+
118+
These settings can have undesired effects and are not recommended to be changed, but there might be certain scenarios where this is the desired outcome.
119+
120+
#### Calculate with incomplete data
121+
122+
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 look ahead hours set to 4 hours, 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 4 hours worth of data if available, otherwise it will evaluate with whatever data is available (in this scenario 2 hours between 10pm and 12am).
123+
116124
## Attributes
117125

118126
The following attributes are available on each sensor
@@ -141,6 +149,7 @@ The following attributes are available on each sensor
141149
| `next_min_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. |
142150
| `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. |
143151
| `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 timeframes won't be calculated. |
152+
| `calculate_with_incomplete_data` | boolean | Determines if calculations should occur when there isn't enough data to satisfy the look ahead hours |
144153

145154
## Services
146155

_docs/setup/target_timeframe.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ Each slot weighting/multiplier must be a whole number or decimal number and be p
123123

124124
You can also use weightings/multipliers to ignore slots. This can be done by assigning a value of 0 for the desired slot.
125125

126+
### Dangerous settings
127+
128+
These settings can have undesired effects and are not recommended to be changed, but there might be certain scenarios where this is the desired outcome.
129+
130+
#### Calculate with incomplete data
131+
132+
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).
133+
126134
## Attributes
127135

128136
The following attributes are available on each sensor
@@ -154,6 +162,7 @@ The following attributes are available on each sensor
154162
| `next_min_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. |
155163
| `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. |
156164
| `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. |
165+
| `calculate_with_incomplete_data` | boolean | Determines if calculations should occur when there isn't enough data to satisfy the look ahead hours |
157166

158167
## Services
159168

custom_components/target_timeframes/const.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import voluptuous as vol
22
import homeassistant.helpers.config_validation as cv
33
from homeassistant.helpers import selector
4+
from homeassistant.data_entry_flow import section
45

56
DOMAIN = "target_timeframes"
67
INTEGRATION_VERSION = "1.2.1"
@@ -37,6 +38,8 @@
3738
CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST = "all_target_times_in_past"
3839
CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST = "all_target_times_in_future_or_past"
3940
CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS = "always"
41+
CONFIG_TARGET_DANGEROUS_SETTINGS = "dangerous_settings"
42+
CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA = "calculate_with_incomplete_data"
4043

4144
CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD = "look_ahead_hours"
4245

@@ -54,7 +57,8 @@
5457
CONFIG_TARGET_MAX_VALUE,
5558
CONFIG_TARGET_WEIGHTING,
5659
CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD,
57-
CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE
60+
CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE,
61+
CONFIG_TARGET_DANGEROUS_SETTINGS
5862
]
5963

6064
REGEX_HOURS = "^[0-9]+(\\.[0-9]+)*$"
@@ -112,9 +116,17 @@
112116
vol.Optional(CONFIG_TARGET_ROLLING_TARGET, default=False): bool,
113117
vol.Optional(CONFIG_TARGET_LATEST_VALUES, default=False): bool,
114118
vol.Optional(CONFIG_TARGET_FIND_HIGHEST_VALUES, default=False): bool,
115-
vol.Optional(CONFIG_TARGET_MIN_VALUE): vol.Coerce(float),
116-
vol.Optional(CONFIG_TARGET_MAX_VALUE): vol.Coerce(float),
119+
vol.Optional(CONFIG_TARGET_MIN_VALUE): float,
120+
vol.Optional(CONFIG_TARGET_MAX_VALUE): float,
117121
vol.Optional(CONFIG_TARGET_WEIGHTING): str,
122+
vol.Required(CONFIG_TARGET_DANGEROUS_SETTINGS): section(
123+
vol.Schema(
124+
{
125+
vol.Required(CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA, default=False): bool,
126+
}
127+
),
128+
{"collapsed": True},
129+
),
118130
})
119131

120132
DATA_SCHEMA_ROLLING_TARGET_TIME_PERIOD = vol.Schema({
@@ -156,6 +168,14 @@
156168
vol.Optional(CONFIG_TARGET_MIN_VALUE): float,
157169
vol.Optional(CONFIG_TARGET_MAX_VALUE): float,
158170
vol.Optional(CONFIG_TARGET_WEIGHTING): str,
171+
vol.Required(CONFIG_TARGET_DANGEROUS_SETTINGS): section(
172+
vol.Schema(
173+
{
174+
vol.Required(CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA, default=False): bool,
175+
}
176+
),
177+
{"collapsed": True},
178+
),
159179
})
160180

161181
EVENT_DATA_SOURCE = "target_time_period_data_source_updated"

custom_components/target_timeframes/entities/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def get_start_and_end_times(current_date: datetime, target_start_time: str, targ
7676

7777
return (target_start, target_end)
7878

79-
def get_fixed_applicable_time_periods(target_start: datetime, target_end: datetime, time_period_values: list, context: str = None):
79+
def get_fixed_applicable_time_periods(target_start: datetime, target_end: datetime, time_period_values: list, calculate_with_incomplete_data = False, context: str = None):
8080
_LOGGER.debug(f'{context} - Finding rates between {target_start} and {target_end}')
8181

8282
# Retrieve the rates that are applicable for our target rate
@@ -92,13 +92,13 @@ def get_fixed_applicable_time_periods(target_start: datetime, target_end: dateti
9292
date_diff = target_end - target_start
9393
hours = (date_diff.days * 24) + (date_diff.seconds // 3600)
9494
periods = hours * 2
95-
if len(applicable_rates) < periods:
95+
if len(applicable_rates) < periods and calculate_with_incomplete_data == False:
9696
_LOGGER.debug(f'{context} - Incorrect number of periods discovered. Require {periods}, but only have {len(applicable_rates)}')
9797
return None
9898

9999
return applicable_rates
100100

101-
def get_rolling_applicable_time_periods(current_date: datetime, time_period_values: list, target_hours: float, context: str = None):
101+
def get_rolling_applicable_time_periods(current_date: datetime, time_period_values: list, target_hours: float, calculate_with_incomplete_data = False, context: str = None):
102102
# Retrieve the rates that are applicable for our target rate
103103
applicable_time_periods = []
104104
periods = target_hours * 2
@@ -113,7 +113,7 @@ def get_rolling_applicable_time_periods(current_date: datetime, time_period_valu
113113
break
114114

115115
# Make sure that we have enough rates that meet our target period
116-
if len(applicable_time_periods) < periods:
116+
if len(applicable_time_periods) < periods and calculate_with_incomplete_data == False:
117117
_LOGGER.debug(f'{context} - Incorrect number of periods discovered. Require {periods}, but only have {len(applicable_time_periods)}')
118118
return None
119119

custom_components/target_timeframes/entities/rolling_target_timeframe.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
from ..const import (
2222
CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD,
23+
CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA,
24+
CONFIG_TARGET_DANGEROUS_SETTINGS,
2325
CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE,
2426
CONFIG_TARGET_HOURS_MODE,
2527
CONFIG_TARGET_MAX_VALUE,
@@ -69,16 +71,7 @@ def __init__(self, hass: HomeAssistant, data_source_id: str, config_entry, confi
6971
self._last_evaluated = None
7072
self._data_source_id = data_source_id
7173
self._attributes["data_source_id"] = self._data_source_id
72-
73-
is_rolling_target = True
74-
if CONFIG_TARGET_ROLLING_TARGET in self._config:
75-
is_rolling_target = self._config[CONFIG_TARGET_ROLLING_TARGET]
76-
self._attributes[CONFIG_TARGET_ROLLING_TARGET] = is_rolling_target
77-
78-
find_last_rates = False
79-
if CONFIG_TARGET_LATEST_VALUES in self._config:
80-
find_last_rates = self._config[CONFIG_TARGET_LATEST_VALUES]
81-
self._attributes[CONFIG_TARGET_LATEST_VALUES] = find_last_rates
74+
self.update_default_attributes()
8275

8376
self._data_source_data = initial_data if initial_data is not None else []
8477
self._target_timeframes = []
@@ -150,10 +143,15 @@ async def async_update(self):
150143
if CONFIG_TARGET_MAX_VALUE in self._config:
151144
max_value = self._config[CONFIG_TARGET_MAX_VALUE]
152145

146+
calculate_with_incomplete_data = False
147+
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._config and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in self._config[CONFIG_TARGET_DANGEROUS_SETTINGS]:
148+
calculate_with_incomplete_data = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
149+
153150
applicable_time_periods = get_rolling_applicable_time_periods(
154151
current_local_date,
155152
self._data_source_data,
156153
self._config[CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD],
154+
calculate_with_incomplete_data,
157155
self._config[CONFIG_TARGET_NAME]
158156
)
159157

@@ -191,7 +189,7 @@ async def async_update(self):
191189
self._attributes["target_times_last_evaluated"] = current_date
192190
_LOGGER.debug(f"{self._config[CONFIG_TARGET_NAME]} - calculated rates: {self._target_timeframes}")
193191

194-
self._attributes["time_periods_incomplete"] = applicable_time_periods is None
192+
self._attributes["time_periods_incomplete"] = applicable_time_periods is None or len(applicable_time_periods) < (target_hours * 2)
195193

196194
active_result = get_target_time_period_info(current_date, self._target_timeframes, offset)
197195

@@ -242,6 +240,7 @@ async def async_added_to_hass(self):
242240
if compare_config(self._config, self._attributes) == False:
243241
self._state = False
244242
self._attributes = self._config.copy()
243+
self.update_default_attributes()
245244
self._target_timeframes = None
246245

247246
_LOGGER.debug(f'{self._config[CONFIG_TARGET_NAME]} - Restored state: {self._state}')
@@ -300,6 +299,7 @@ async def async_update_rolling_target_timeframe_config(self, target_hours=None,
300299

301300
self._config = config
302301
self._attributes = self._config.copy()
302+
self.update_default_attributes()
303303
self._target_timeframes = []
304304
await self.async_update()
305305
self.async_write_ha_state()
@@ -313,4 +313,24 @@ async def async_update_rolling_target_timeframe_config(self, target_hours=None,
313313
self._config_entry,
314314
self._config_subentry,
315315
data = new_config_data
316-
)
316+
)
317+
318+
def update_default_attributes(self):
319+
"""Update the default attributes."""
320+
self._attributes["data_source_id"] = self._data_source_id
321+
322+
is_rolling_target = True
323+
if CONFIG_TARGET_ROLLING_TARGET in self._config:
324+
is_rolling_target = self._config[CONFIG_TARGET_ROLLING_TARGET]
325+
self._attributes[CONFIG_TARGET_ROLLING_TARGET] = is_rolling_target
326+
327+
find_last_rates = False
328+
if CONFIG_TARGET_LATEST_VALUES in self._config:
329+
find_last_rates = self._config[CONFIG_TARGET_LATEST_VALUES]
330+
self._attributes[CONFIG_TARGET_LATEST_VALUES] = find_last_rates
331+
332+
calculate_with_incomplete_data = False
333+
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._config and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in self._config[CONFIG_TARGET_DANGEROUS_SETTINGS]:
334+
calculate_with_incomplete_data = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
335+
del self._attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]
336+
self._attributes[CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA] = calculate_with_incomplete_data

custom_components/target_timeframes/entities/target_timeframe.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from homeassistant.helpers import translation
2121

2222
from ..const import (
23+
CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA,
24+
CONFIG_TARGET_DANGEROUS_SETTINGS,
2325
CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE,
2426
CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST,
2527
CONFIG_TARGET_HOURS_MODE,
@@ -73,17 +75,8 @@ def __init__(self, hass: HomeAssistant, data_source_id: str, config_entry, confi
7375
self._attributes = self._config.copy()
7476
self._last_evaluated = None
7577
self._data_source_id = data_source_id
76-
self._attributes["data_source_id"] = self._data_source_id
77-
78-
is_rolling_target = True
79-
if CONFIG_TARGET_ROLLING_TARGET in self._config:
80-
is_rolling_target = self._config[CONFIG_TARGET_ROLLING_TARGET]
81-
self._attributes[CONFIG_TARGET_ROLLING_TARGET] = is_rolling_target
8278

83-
find_last_rates = False
84-
if CONFIG_TARGET_LATEST_VALUES in self._config:
85-
find_last_rates = self._config[CONFIG_TARGET_LATEST_VALUES]
86-
self._attributes[CONFIG_TARGET_LATEST_VALUES] = find_last_rates
79+
self.update_default_attributes()
8780

8881
self._data_source_data = initial_data if initial_data is not None else []
8982
self._target_timeframes = []
@@ -167,11 +160,16 @@ async def async_update(self):
167160
if CONFIG_TARGET_MAX_VALUE in self._config:
168161
max_rate = self._config[CONFIG_TARGET_MAX_VALUE]
169162

163+
calculate_with_incomplete_data = False
164+
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._config and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in self._config[CONFIG_TARGET_DANGEROUS_SETTINGS]:
165+
calculate_with_incomplete_data = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
166+
170167
target_start, target_end = get_start_and_end_times(current_local_date, start_time, end_time, True, self._config[CONFIG_TARGET_NAME])
171168
applicable_time_periods = get_fixed_applicable_time_periods(
172169
target_start,
173170
target_end,
174171
self._data_source_data,
172+
calculate_with_incomplete_data,
175173
self._config[CONFIG_TARGET_NAME]
176174
)
177175

@@ -220,7 +218,7 @@ async def async_update(self):
220218
self._attributes["target_times_last_evaluated"] = current_date
221219
_LOGGER.debug(f"{self._config[CONFIG_TARGET_NAME]} - calculated rates: {self._target_timeframes}")
222220

223-
self._attributes["time_periods_incomplete"] = applicable_time_periods is None
221+
self._attributes["time_periods_incomplete"] = applicable_time_periods is None or len(applicable_time_periods) < (target_hours * 2)
224222

225223
active_result = get_target_time_period_info(current_date, self._target_timeframes, offset)
226224

@@ -271,6 +269,7 @@ async def async_added_to_hass(self):
271269
if compare_config(self._config, self._attributes) == False:
272270
self._state = False
273271
self._attributes = self._config.copy()
272+
self.update_default_attributes()
274273
self._target_timeframes = None
275274

276275
_LOGGER.debug(f'{self._config[CONFIG_TARGET_NAME]} - Restored state: {self._state}')
@@ -335,6 +334,7 @@ async def async_update_target_timeframe_config(self, target_start_time=None, tar
335334

336335
self._config = config
337336
self._attributes = self._config.copy()
337+
self.update_default_attributes()
338338
self._target_timeframes = []
339339
await self.async_update()
340340
self.async_write_ha_state()
@@ -348,4 +348,24 @@ async def async_update_target_timeframe_config(self, target_start_time=None, tar
348348
self._config_entry,
349349
self._config_subentry,
350350
data = new_config_data
351-
)
351+
)
352+
353+
def update_default_attributes(self):
354+
"""Update the default attributes."""
355+
self._attributes["data_source_id"] = self._data_source_id
356+
357+
is_rolling_target = True
358+
if CONFIG_TARGET_ROLLING_TARGET in self._config:
359+
is_rolling_target = self._config[CONFIG_TARGET_ROLLING_TARGET]
360+
self._attributes[CONFIG_TARGET_ROLLING_TARGET] = is_rolling_target
361+
362+
find_last_rates = False
363+
if CONFIG_TARGET_LATEST_VALUES in self._config:
364+
find_last_rates = self._config[CONFIG_TARGET_LATEST_VALUES]
365+
self._attributes[CONFIG_TARGET_LATEST_VALUES] = find_last_rates
366+
367+
calculate_with_incomplete_data = False
368+
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._config and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in self._config[CONFIG_TARGET_DANGEROUS_SETTINGS]:
369+
calculate_with_incomplete_data = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
370+
del self._attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]
371+
self._attributes[CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA] = calculate_with_incomplete_data

0 commit comments

Comments
 (0)