Skip to content
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
3 changes: 2 additions & 1 deletion custom_components/target_timeframes/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
CONFIG_TARGET_WEIGHTING,
CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD,
CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE,
CONFIG_TARGET_DANGEROUS_SETTINGS
CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA,
CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT
]

REGEX_HOURS = "^[0-9]+(\\.[0-9]+)*$"
Expand Down
13 changes: 7 additions & 6 deletions custom_components/target_timeframes/entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

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

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

_LOGGER = logging.getLogger(__name__)

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

return weighting

def compare_config(current_config: dict, existing_config: dict):
if current_config is None or existing_config is None:
def compare_config_to_attributes(current_config: dict, existing_attributes: dict):
if current_config is None or existing_attributes is None:
return False

for key in CONFIG_TARGET_KEYS:
if ((key not in existing_config and key in current_config) or
(key in existing_config and key not in current_config) or
(key in existing_config and key in current_config and current_config[key] != existing_config[key])):
if ((key not in existing_attributes and key in current_config) or
(key in existing_attributes and key not in current_config) or
(key in existing_attributes and key in current_config and current_config[key] != existing_attributes[key])):
_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}"')
return False

return True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
from . import (
calculate_continuous_times,
calculate_intermittent_times,
compare_config,
compare_config_to_attributes,
create_weighting,
extract_config,
get_rolling_applicable_time_periods,
Expand Down Expand Up @@ -238,13 +238,15 @@ async def async_added_to_hass(self):
self._state = None if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) or state.state is None else state.state.lower() == 'on'
self._attributes = dict_to_typed_dict(
state.attributes,
[]
[CONFIG_TARGET_ROLLING_TARGET] # This was incorrectly included
)
self.update_default_attributes()

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

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

def update_default_attributes(self):
"""Update the default attributes."""
self._attributes["data_source_id"] = self._data_source_id

is_rolling_target = True
if CONFIG_TARGET_ROLLING_TARGET in self._config:
is_rolling_target = self._config[CONFIG_TARGET_ROLLING_TARGET]
self._attributes[CONFIG_TARGET_ROLLING_TARGET] = is_rolling_target
def expand_config_attributes(self, attributes: dict):
new_attributes = attributes.copy()

find_last_rates = False
if CONFIG_TARGET_LATEST_VALUES in self._config:
find_last_rates = self._config[CONFIG_TARGET_LATEST_VALUES]
self._attributes[CONFIG_TARGET_LATEST_VALUES] = find_last_rates
if CONFIG_TARGET_LATEST_VALUES in new_attributes:
find_last_rates = new_attributes[CONFIG_TARGET_LATEST_VALUES]
new_attributes[CONFIG_TARGET_LATEST_VALUES] = find_last_rates

if CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA not in new_attributes:
calculate_with_incomplete_data = False
if CONFIG_TARGET_DANGEROUS_SETTINGS in new_attributes and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]:
calculate_with_incomplete_data = new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
new_attributes[CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA] = calculate_with_incomplete_data

calculate_with_incomplete_data = False
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._config and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in self._config[CONFIG_TARGET_DANGEROUS_SETTINGS]:
calculate_with_incomplete_data = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
self._attributes[CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA] = calculate_with_incomplete_data
if CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT not in new_attributes:
minimum_required_minutes_in_slot = CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT
if CONFIG_TARGET_DANGEROUS_SETTINGS in new_attributes and CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT in new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]:
minimum_required_minutes_in_slot = new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT]
new_attributes[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] = minimum_required_minutes_in_slot

if CONFIG_TARGET_DANGEROUS_SETTINGS in self._attributes:
del self._attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]
if CONFIG_TARGET_DANGEROUS_SETTINGS in new_attributes:
del new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]

return new_attributes

def update_default_attributes(self):
"""Update the default attributes."""
self._attributes["data_source_id"] = self._data_source_id
self._attributes = self.expand_config_attributes(self._attributes)
59 changes: 34 additions & 25 deletions custom_components/target_timeframes/entities/target_timeframe.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
from datetime import timedelta
import math

import voluptuous as vol
Expand Down Expand Up @@ -48,7 +47,7 @@
from . import (
calculate_continuous_times,
calculate_intermittent_times,
compare_config,
compare_config_to_attributes,
create_weighting,
extract_config,
get_fixed_applicable_time_periods,
Expand Down Expand Up @@ -275,11 +274,13 @@ async def async_added_to_hass(self):
state.attributes,
[]
)
self.update_default_attributes()

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

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

def update_default_attributes(self):
"""Update the default attributes."""
self._attributes["data_source_id"] = self._data_source_id
def expand_config_attributes(self, attributes: dict):
new_attributes = attributes.copy()

is_rolling_target = True
if CONFIG_TARGET_ROLLING_TARGET in self._config:
is_rolling_target = self._config[CONFIG_TARGET_ROLLING_TARGET]
self._attributes[CONFIG_TARGET_ROLLING_TARGET] = is_rolling_target
if CONFIG_TARGET_ROLLING_TARGET in new_attributes:
is_rolling_target = new_attributes[CONFIG_TARGET_ROLLING_TARGET]
new_attributes[CONFIG_TARGET_ROLLING_TARGET] = is_rolling_target

find_last_rates = False
if CONFIG_TARGET_LATEST_VALUES in self._config:
find_last_rates = self._config[CONFIG_TARGET_LATEST_VALUES]
self._attributes[CONFIG_TARGET_LATEST_VALUES] = find_last_rates

calculate_with_incomplete_data = False
if CONFIG_TARGET_DANGEROUS_SETTINGS in self._config and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in self._config[CONFIG_TARGET_DANGEROUS_SETTINGS]:
calculate_with_incomplete_data = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
self._attributes[CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA] = calculate_with_incomplete_data

minimum_required_minutes_in_slot = CONFIG_TARGET_DEFAULT_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]:
minimum_required_minutes_in_slot = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT]
self._attributes[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] = minimum_required_minutes_in_slot

if CONFIG_TARGET_DANGEROUS_SETTINGS in self._attributes:
del self._attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]
if CONFIG_TARGET_LATEST_VALUES in new_attributes:
find_last_rates = new_attributes[CONFIG_TARGET_LATEST_VALUES]
new_attributes[CONFIG_TARGET_LATEST_VALUES] = find_last_rates

if CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA not in new_attributes:
calculate_with_incomplete_data = False
if CONFIG_TARGET_DANGEROUS_SETTINGS in new_attributes and CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA in new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]:
calculate_with_incomplete_data = new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA]
new_attributes[CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA] = calculate_with_incomplete_data

if CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT not in new_attributes:
minimum_required_minutes_in_slot = CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT
if CONFIG_TARGET_DANGEROUS_SETTINGS in new_attributes and CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT in new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]:
minimum_required_minutes_in_slot = new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT]
new_attributes[CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] = minimum_required_minutes_in_slot

if CONFIG_TARGET_DANGEROUS_SETTINGS in new_attributes:
del new_attributes[CONFIG_TARGET_DANGEROUS_SETTINGS]

return new_attributes

def update_default_attributes(self):
"""Update the default attributes."""
self._attributes["data_source_id"] = self._data_source_id
self._attributes = self.expand_config_attributes(self._attributes)
23 changes: 0 additions & 23 deletions tests/unit/target_rates/test_compare_config.py

This file was deleted.

27 changes: 27 additions & 0 deletions tests/unit/target_rates/test_compare_config_to_attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pytest

from custom_components.target_timeframes.entities import compare_config_to_attributes
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

@pytest.mark.asyncio
@pytest.mark.parametrize("attributes,expected_result",[
(None, False),
({}, False),
({ 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),
({ 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),
({ 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),
({ 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),
({ 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),
({ 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),
({ 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),
])
async def test_when_config_is_compared_to_attributes_then_expected_value_is_returned(attributes, expected_result):
current_config = {
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
}

actual_result = compare_config_to_attributes(current_config, attributes)
assert actual_result == expected_result