Skip to content

Commit 8bf07a1

Browse files
committed
fix: Fixed issue where discovered target times were being reset incorrectly on reload (2 hours dev time)
1 parent 14007c6 commit 8bf07a1

File tree

6 files changed

+100
-75
lines changed

6 files changed

+100
-75
lines changed

custom_components/target_timeframes/const.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@
6060
CONFIG_TARGET_WEIGHTING,
6161
CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD,
6262
CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE,
63-
CONFIG_TARGET_DANGEROUS_SETTINGS
63+
CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA,
64+
CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT
6465
]
6566

6667
REGEX_HOURS = "^[0-9]+(\\.[0-9]+)*$"

custom_components/target_timeframes/entities/__init__.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from homeassistant.util.dt import (as_utc, parse_datetime)
88

9-
from ..const import CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT, CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS, CONFIG_TARGET_HOURS_MODE_EXACT, CONFIG_TARGET_HOURS_MODE_MAXIMUM, CONFIG_TARGET_HOURS_MODE_MINIMUM, CONFIG_TARGET_KEYS, REGEX_OFFSET_PARTS, REGEX_WEIGHTING
9+
from ..const import CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA, CONFIG_TARGET_DANGEROUS_SETTINGS, CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT, CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT, CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS, CONFIG_TARGET_HOURS_MODE_EXACT, CONFIG_TARGET_HOURS_MODE_MAXIMUM, CONFIG_TARGET_HOURS_MODE_MINIMUM, CONFIG_TARGET_KEYS, REGEX_OFFSET_PARTS, REGEX_WEIGHTING
1010

1111
_LOGGER = logging.getLogger(__name__)
1212

@@ -423,14 +423,15 @@ def create_weighting(config: str, number_of_slots: int):
423423

424424
return weighting
425425

426-
def compare_config(current_config: dict, existing_config: dict):
427-
if current_config is None or existing_config is None:
426+
def compare_config_to_attributes(current_config: dict, existing_attributes: dict):
427+
if current_config is None or existing_attributes is None:
428428
return False
429429

430430
for key in CONFIG_TARGET_KEYS:
431-
if ((key not in existing_config and key in current_config) or
432-
(key in existing_config and key not in current_config) or
433-
(key in existing_config and key in current_config and current_config[key] != existing_config[key])):
431+
if ((key not in existing_attributes and key in current_config) or
432+
(key in existing_attributes and key not in current_config) or
433+
(key in existing_attributes and key in current_config and current_config[key] != existing_attributes[key])):
434+
_LOGGER.debug(f'Configuration key "{key}" has changed from "{existing_attributes[key] if key in existing_attributes else None}" to "{current_config[key] if key in current_config else None}"')
434435
return False
435436

436437
return True

custom_components/target_timeframes/entities/rolling_target_timeframe.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
from . import (
4646
calculate_continuous_times,
4747
calculate_intermittent_times,
48-
compare_config,
48+
compare_config_to_attributes,
4949
create_weighting,
5050
extract_config,
5151
get_rolling_applicable_time_periods,
@@ -238,13 +238,15 @@ async def async_added_to_hass(self):
238238
self._state = None if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) or state.state is None else state.state.lower() == 'on'
239239
self._attributes = dict_to_typed_dict(
240240
state.attributes,
241-
[]
241+
[CONFIG_TARGET_ROLLING_TARGET] # This was incorrectly included
242242
)
243+
self.update_default_attributes()
243244

244245
self._target_timeframes = self._attributes["target_times"] if "target_times" in self._attributes else []
245246

246247
# Reset everything if our settings have changed
247-
if compare_config(self._config, self._attributes) == False:
248+
if compare_config_to_attributes(self.expand_config_attributes(self._config), self._attributes) == False:
249+
_LOGGER.debug(f'Not restoring target times for {self._config[CONFIG_TARGET_NAME]} as attributes have changed')
248250
self._state = False
249251
self._attributes = self._config.copy()
250252
self.update_default_attributes()
@@ -322,24 +324,32 @@ async def async_update_rolling_target_timeframe_config(self, target_hours=None,
322324
data = new_config_data
323325
)
324326

325-
def update_default_attributes(self):
326-
"""Update the default attributes."""
327-
self._attributes["data_source_id"] = self._data_source_id
328-
329-
is_rolling_target = True
330-
if CONFIG_TARGET_ROLLING_TARGET in self._config:
331-
is_rolling_target = self._config[CONFIG_TARGET_ROLLING_TARGET]
332-
self._attributes[CONFIG_TARGET_ROLLING_TARGET] = is_rolling_target
327+
def expand_config_attributes(self, attributes: dict):
328+
new_attributes = attributes.copy()
333329

334330
find_last_rates = False
335-
if CONFIG_TARGET_LATEST_VALUES in self._config:
336-
find_last_rates = self._config[CONFIG_TARGET_LATEST_VALUES]
337-
self._attributes[CONFIG_TARGET_LATEST_VALUES] = find_last_rates
331+
if CONFIG_TARGET_LATEST_VALUES in new_attributes:
332+
find_last_rates = new_attributes[CONFIG_TARGET_LATEST_VALUES]
333+
new_attributes[CONFIG_TARGET_LATEST_VALUES] = find_last_rates
334+
335+
if CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA not in new_attributes:
336+
calculate_with_incomplete_data = False
337+
if CONFIG_TARGET_DANGEROUS_SETTINGS in new_attributes and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]:
338+
calculate_with_incomplete_data = new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
339+
new_attributes[CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA] = calculate_with_incomplete_data
338340

339-
calculate_with_incomplete_data = False
340-
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._config and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in self._config[CONFIG_TARGET_DANGEROUS_SETTINGS]:
341-
calculate_with_incomplete_data = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
342-
self._attributes[CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA] = calculate_with_incomplete_data
341+
if CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT not in new_attributes:
342+
minimum_required_minutes_in_slot = CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT
343+
if CONFIG_TARGET_DANGEROUS_SETTINGS in new_attributes and CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT in new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]:
344+
minimum_required_minutes_in_slot = new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT]
345+
new_attributes[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] = minimum_required_minutes_in_slot
343346

344-
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._attributes:
345-
del self._attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]
347+
if CONFIG_TARGET_DANGEROUS_SETTINGS in new_attributes:
348+
del new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]
349+
350+
return new_attributes
351+
352+
def update_default_attributes(self):
353+
"""Update the default attributes."""
354+
self._attributes["data_source_id"] = self._data_source_id
355+
self._attributes = self.expand_config_attributes(self._attributes)

custom_components/target_timeframes/entities/target_timeframe.py

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
from datetime import timedelta
32
import math
43

54
import voluptuous as vol
@@ -48,7 +47,7 @@
4847
from . import (
4948
calculate_continuous_times,
5049
calculate_intermittent_times,
51-
compare_config,
50+
compare_config_to_attributes,
5251
create_weighting,
5352
extract_config,
5453
get_fixed_applicable_time_periods,
@@ -275,11 +274,13 @@ async def async_added_to_hass(self):
275274
state.attributes,
276275
[]
277276
)
277+
self.update_default_attributes()
278278

279279
self._target_timeframes = self._attributes["target_times"] if "target_times" in self._attributes else []
280280

281281
# Reset everything if our settings have changed
282-
if compare_config(self._config, self._attributes) == False:
282+
if compare_config_to_attributes(self.expand_config_attributes(self._config), self._attributes) == False:
283+
_LOGGER.debug(f'Not restoring target times for {self._config[CONFIG_TARGET_NAME]} as attributes have changed')
283284
self._state = False
284285
self._attributes = self._config.copy()
285286
self.update_default_attributes()
@@ -363,29 +364,37 @@ async def async_update_target_timeframe_config(self, target_start_time=None, tar
363364
data = new_config_data
364365
)
365366

366-
def update_default_attributes(self):
367-
"""Update the default attributes."""
368-
self._attributes["data_source_id"] = self._data_source_id
367+
def expand_config_attributes(self, attributes: dict):
368+
new_attributes = attributes.copy()
369369

370370
is_rolling_target = True
371-
if CONFIG_TARGET_ROLLING_TARGET in self._config:
372-
is_rolling_target = self._config[CONFIG_TARGET_ROLLING_TARGET]
373-
self._attributes[CONFIG_TARGET_ROLLING_TARGET] = is_rolling_target
371+
if CONFIG_TARGET_ROLLING_TARGET in new_attributes:
372+
is_rolling_target = new_attributes[CONFIG_TARGET_ROLLING_TARGET]
373+
new_attributes[CONFIG_TARGET_ROLLING_TARGET] = is_rolling_target
374374

375375
find_last_rates = False
376-
if CONFIG_TARGET_LATEST_VALUES in self._config:
377-
find_last_rates = self._config[CONFIG_TARGET_LATEST_VALUES]
378-
self._attributes[CONFIG_TARGET_LATEST_VALUES] = find_last_rates
379-
380-
calculate_with_incomplete_data = False
381-
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._config and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in self._config[CONFIG_TARGET_DANGEROUS_SETTINGS]:
382-
calculate_with_incomplete_data = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
383-
self._attributes[CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA] = calculate_with_incomplete_data
384-
385-
minimum_required_minutes_in_slot = CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT
386-
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._config and CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT in self._config[CONFIG_TARGET_DANGEROUS_SETTINGS]:
387-
minimum_required_minutes_in_slot = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT]
388-
self._attributes[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] = minimum_required_minutes_in_slot
389-
390-
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._attributes:
391-
del self._attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]
376+
if CONFIG_TARGET_LATEST_VALUES in new_attributes:
377+
find_last_rates = new_attributes[CONFIG_TARGET_LATEST_VALUES]
378+
new_attributes[CONFIG_TARGET_LATEST_VALUES] = find_last_rates
379+
380+
if CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA not in new_attributes:
381+
calculate_with_incomplete_data = False
382+
if CONFIG_TARGET_DANGEROUS_SETTINGS in new_attributes and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]:
383+
calculate_with_incomplete_data = new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
384+
new_attributes[CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA] = calculate_with_incomplete_data
385+
386+
if CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT not in new_attributes:
387+
minimum_required_minutes_in_slot = CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT
388+
if CONFIG_TARGET_DANGEROUS_SETTINGS in new_attributes and CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT in new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]:
389+
minimum_required_minutes_in_slot = new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT]
390+
new_attributes[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] = minimum_required_minutes_in_slot
391+
392+
if CONFIG_TARGET_DANGEROUS_SETTINGS in new_attributes:
393+
del new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]
394+
395+
return new_attributes
396+
397+
def update_default_attributes(self):
398+
"""Update the default attributes."""
399+
self._attributes["data_source_id"] = self._data_source_id
400+
self._attributes = self.expand_config_attributes(self._attributes)

tests/unit/target_rates/test_compare_config.py

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import pytest
2+
3+
from custom_components.target_timeframes.entities import compare_config_to_attributes
4+
from custom_components.target_timeframes.const import CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA, CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT, CONFIG_TARGET_HOURS, CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT, CONFIG_TARGET_TYPE
5+
6+
@pytest.mark.asyncio
7+
@pytest.mark.parametrize("attributes,expected_result",[
8+
(None, False),
9+
({}, False),
10+
({ CONFIG_TARGET_HOURS: 1, CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA: False, CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT }, False),
11+
({ CONFIG_TARGET_HOURS: 2, CONFIG_TARGET_TYPE: "Continuous", CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA: False, CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT }, False),
12+
({ CONFIG_TARGET_HOURS: 1, CONFIG_TARGET_TYPE: "Intermittent", CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA: False, CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT }, False),
13+
({ CONFIG_TARGET_HOURS: 1, CONFIG_TARGET_TYPE: "Continuous", CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA: True, CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT }, False),
14+
({ CONFIG_TARGET_HOURS: 1, CONFIG_TARGET_TYPE: "Continuous", CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA: False, CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT + 1 }, False),
15+
({ CONFIG_TARGET_HOURS: 1, CONFIG_TARGET_TYPE: "Continuous", CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA: False, CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT }, True),
16+
({ CONFIG_TARGET_HOURS: 1, CONFIG_TARGET_TYPE: "Continuous", "Something": "else", CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA: False, CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT }, True),
17+
])
18+
async def test_when_config_is_compared_to_attributes_then_expected_value_is_returned(attributes, expected_result):
19+
current_config = {
20+
CONFIG_TARGET_HOURS: 1,
21+
CONFIG_TARGET_TYPE: "Continuous",
22+
CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA: False,
23+
CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT
24+
}
25+
26+
actual_result = compare_config_to_attributes(current_config, attributes)
27+
assert actual_result == expected_result

0 commit comments

Comments
 (0)