Skip to content

Commit 6627bb8

Browse files
committed
fix: Fixed issue where target times could be recalculated within the same target time frame once complete, causing the sensor to come on more than it should (1 hour dev time)
1 parent 04e95ad commit 6627bb8

File tree

4 files changed

+180
-4
lines changed

4 files changed

+180
-4
lines changed

custom_components/target_timeframes/entities/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ def apply_offset(date_time: datetime, offset: str, inverse = False):
3333

3434
return date_time + timedelta(hours=hours, minutes=minutes, seconds=seconds)
3535

36+
def is_target_timeframe_complete_in_period(current_date: datetime, applicable_time_periods: list | None, target_timeframes: list | None):
37+
if applicable_time_periods is None or target_timeframes is None or len(applicable_time_periods) < 1 or len(target_timeframes) < 1:
38+
return False
39+
40+
return (
41+
applicable_time_periods[0]["start"] <= target_timeframes[0]["start"] and
42+
applicable_time_periods[-1]["end"] >= target_timeframes[-1]["end"] and
43+
target_timeframes[-1]["end"] <= current_date
44+
)
45+
3646
def get_fixed_applicable_time_periods(current_date: datetime, target_start_time: str, target_end_time: str, time_period_values: list, is_rolling_target = True):
3747
if (target_start_time is not None):
3848
target_start = parse_datetime(current_date.strftime(f"%Y-%m-%dT{target_start_time}:00%z"))

custom_components/target_timeframes/entities/data_source.py

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

33
from homeassistant.core import HomeAssistant, callback
44
from homeassistant.exceptions import ServiceValidationError
5-
from homeassistant.util.dt import (utcnow)
5+
from homeassistant.util.dt import (utcnow, now)
66

77
from homeassistant.const import (
88
STATE_UNAVAILABLE,
@@ -98,7 +98,7 @@ async def async_update_target_timeframe_data_source(self, data, replace_all_exis
9898
result.data
9999
if replace_all_existing_data
100100
else merge_data_source_data(
101-
utcnow(),
101+
now(),
102102
result.data,
103103
list(map(lambda x: DataSourceItem.parse_obj(x), self._attributes["data"]))
104104
if "data" in self._attributes

custom_components/target_timeframes/entities/target_timeframe.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
extract_config,
5050
get_fixed_applicable_time_periods,
5151
get_target_time_period_info,
52+
is_target_timeframe_complete_in_period,
5253
should_evaluate_target_timeframes
5354
)
5455

@@ -128,7 +129,8 @@ async def async_update(self):
128129
# Find the current rate. Rates change a maximum of once every 30 minutes.
129130
current_date = utcnow()
130131

131-
should_evaluate = should_evaluate_target_timeframes(current_date, self._target_timeframes, self._config[CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE] if CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE in self._config else CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST)
132+
evaluation_mode = self._config[CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE] if CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE in self._config else CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST
133+
should_evaluate = should_evaluate_target_timeframes(current_date, self._target_timeframes, evaluation_mode)
132134
if should_evaluate:
133135
_LOGGER.debug(f'{len(self._data_source_data) if self._data_source_data is not None else None} time periods found')
134136

@@ -172,7 +174,9 @@ async def async_update(self):
172174
is_rolling_target
173175
)
174176

175-
if applicable_time_periods is not None:
177+
is_target_timeframe_complete = is_rolling_target == False and is_target_timeframe_complete_in_period(current_local_date, applicable_time_periods, self._target_timeframes)
178+
179+
if applicable_time_periods is not None and is_target_timeframe_complete == False:
176180
number_of_slots = math.ceil(target_hours * 2)
177181
weighting = create_weighting(self._config[CONFIG_TARGET_WEIGHTING] if CONFIG_TARGET_WEIGHTING in self._config else None, number_of_slots)
178182

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
from datetime import datetime, timedelta
2+
import pytest
3+
4+
from custom_components.target_timeframes.entities import is_target_timeframe_complete_in_period
5+
6+
@pytest.mark.asyncio
7+
async def test_when_both_lists_are_none_then_false_is_returned():
8+
# Arrange
9+
current_date = datetime.now()
10+
applicable_time_periods = None
11+
target_timeframes = None
12+
13+
# Act
14+
result = is_target_timeframe_complete_in_period(current_date, applicable_time_periods, target_timeframes)
15+
16+
# Assert
17+
assert result is False
18+
19+
@pytest.mark.asyncio
20+
async def test_when_applicable_time_periods_is_empty_then_false_is_returned():
21+
# Arrange
22+
current_date = datetime.now()
23+
applicable_time_periods = []
24+
target_timeframes = [{"start": datetime.now(), "end": datetime.now() + timedelta(hours=1)}]
25+
26+
# Act
27+
result = is_target_timeframe_complete_in_period(current_date, applicable_time_periods, target_timeframes)
28+
29+
# Assert
30+
assert result is False
31+
32+
@pytest.mark.asyncio
33+
async def test_when_target_timeframes_is_empty_then_false_is_returned():
34+
# Arrange
35+
current_date = datetime.now()
36+
applicable_time_periods = [{"start": datetime.now(), "end": datetime.now() + timedelta(hours=1)}]
37+
target_timeframes = []
38+
39+
# Act
40+
result = is_target_timeframe_complete_in_period(current_date, applicable_time_periods, target_timeframes)
41+
42+
# Assert
43+
assert result is False
44+
45+
@pytest.mark.asyncio
46+
async def test_when_target_timeframe_is_within_applicable_periods_and_complete_then_true_is_returned():
47+
# Arrange
48+
current_date = datetime.now()
49+
start_time = current_date - timedelta(hours=2)
50+
end_time = current_date - timedelta(minutes=30)
51+
52+
applicable_time_periods = [
53+
{"start": start_time - timedelta(minutes=30), "end": start_time + timedelta(minutes=30)},
54+
{"start": start_time + timedelta(minutes=30), "end": start_time + timedelta(minutes=60)},
55+
{"start": start_time + timedelta(minutes=60), "end": end_time + timedelta(minutes=30)}
56+
]
57+
58+
target_timeframes = [
59+
{"start": start_time, "end": start_time + timedelta(minutes=30)},
60+
{"start": start_time + timedelta(minutes=30), "end": end_time}
61+
]
62+
63+
# Act
64+
result = is_target_timeframe_complete_in_period(current_date, applicable_time_periods, target_timeframes)
65+
66+
# Assert
67+
assert result is True
68+
69+
@pytest.mark.asyncio
70+
async def test_when_target_timeframe_start_before_applicable_periods_then_false_is_returned():
71+
# Arrange
72+
current_date = datetime.now()
73+
start_time = current_date - timedelta(hours=2)
74+
end_time = current_date - timedelta(minutes=30)
75+
76+
applicable_time_periods = [
77+
{"start": start_time, "end": start_time + timedelta(minutes=30)},
78+
{"start": start_time + timedelta(minutes=30), "end": start_time + timedelta(minutes=60)},
79+
{"start": start_time + timedelta(minutes=60), "end": end_time}
80+
]
81+
82+
target_timeframes = [
83+
{"start": start_time - timedelta(minutes=30), "end": start_time + timedelta(minutes=30)},
84+
{"start": start_time + timedelta(minutes=30), "end": end_time}
85+
]
86+
87+
# Act
88+
result = is_target_timeframe_complete_in_period(current_date, applicable_time_periods, target_timeframes)
89+
90+
# Assert
91+
assert result is False
92+
93+
@pytest.mark.asyncio
94+
async def test_when_target_timeframe_end_after_applicable_periods_then_false_is_returned():
95+
# Arrange
96+
current_date = datetime.now()
97+
start_time = current_date - timedelta(hours=2)
98+
end_time = current_date - timedelta(minutes=30)
99+
100+
applicable_time_periods = [
101+
{"start": start_time, "end": start_time + timedelta(minutes=30)},
102+
{"start": start_time + timedelta(minutes=30), "end": start_time + timedelta(minutes=60)},
103+
{"start": start_time + timedelta(minutes=60), "end": end_time}
104+
]
105+
106+
target_timeframes = [
107+
{"start": start_time, "end": start_time + timedelta(minutes=30)},
108+
{"start": start_time + timedelta(minutes=30), "end": end_time + timedelta(minutes=30)}
109+
]
110+
111+
# Act
112+
result = is_target_timeframe_complete_in_period(current_date, applicable_time_periods, target_timeframes)
113+
114+
# Assert
115+
assert result is False
116+
117+
@pytest.mark.asyncio
118+
async def test_when_target_timeframe_end_not_in_past_then_false_is_returned():
119+
# Arrange
120+
current_date = datetime.now()
121+
start_time = current_date - timedelta(hours=2)
122+
end_time = current_date + timedelta(minutes=30)
123+
124+
applicable_time_periods = [
125+
{"start": start_time - timedelta(minutes=30), "end": start_time + timedelta(minutes=30)},
126+
{"start": start_time + timedelta(minutes=30), "end": start_time + timedelta(minutes=60)},
127+
{"start": start_time + timedelta(minutes=60), "end": end_time + timedelta(minutes=30)}
128+
]
129+
130+
target_timeframes = [
131+
{"start": start_time, "end": start_time + timedelta(minutes=30)},
132+
{"start": start_time + timedelta(minutes=30), "end": end_time}
133+
]
134+
135+
# Act
136+
result = is_target_timeframe_complete_in_period(current_date, applicable_time_periods, target_timeframes)
137+
138+
# Assert
139+
assert result is False
140+
141+
@pytest.mark.asyncio
142+
async def test_when_target_timeframe_is_exactly_equal_to_applicable_periods_and_complete_then_true_is_returned():
143+
# Arrange
144+
current_date = datetime.now()
145+
start_time = current_date - timedelta(hours=2)
146+
end_time = current_date - timedelta(minutes=30)
147+
148+
applicable_time_periods = [
149+
{"start": start_time, "end": start_time + timedelta(minutes=30)},
150+
{"start": start_time + timedelta(minutes=30), "end": end_time}
151+
]
152+
153+
target_timeframes = [
154+
{"start": start_time, "end": start_time + timedelta(minutes=30)},
155+
{"start": start_time + timedelta(minutes=30), "end": end_time}
156+
]
157+
158+
# Act
159+
result = is_target_timeframe_complete_in_period(current_date, applicable_time_periods, target_timeframes)
160+
161+
# Assert
162+
assert result is True

0 commit comments

Comments
 (0)