diff --git a/custom_components/carbon_intensity/binary_sensors/__init__.py b/custom_components/carbon_intensity/binary_sensors/__init__.py index fc9aceb..68728ea 100644 --- a/custom_components/carbon_intensity/binary_sensors/__init__.py +++ b/custom_components/carbon_intensity/binary_sensors/__init__.py @@ -1,4 +1,4 @@ -from datetime import timedelta +from datetime import datetime, timedelta import math from homeassistant.util.dt import (as_utc, parse_datetime) from ..utils import (apply_offset) @@ -6,7 +6,7 @@ _LOGGER = logging.getLogger(__name__) -def __get_applicable_rates(current_date, target_start_time, target_end_time, rates, target_start_offset, is_rolling_target): +def __get_applicable_rates(current_date: datetime, target_start_time: str, target_end_time: str, rates, is_rolling_target: bool): if (target_start_time is not None): target_start = parse_datetime(current_date.strftime(f"%Y-%m-%dT{target_start_time}:00%z")) else: @@ -32,12 +32,6 @@ def __get_applicable_rates(current_date, target_start_time, target_end_time, rat _LOGGER.debug(f'Rolling target and {target_start} is in the past. Setting start to {current_date}') target_start = current_date - # Apply our offset so we make sure our target turns on within the specified timeframe - if (target_start_offset is not None): - _LOGGER.debug(f'Offsetting time period') - target_start = apply_offset(target_start, target_start_offset, True) - target_end = apply_offset(target_end, target_start_offset, True) - # If our start and end are both in the past, then look to the next day if (target_start < current_date and target_end < current_date): target_start = target_start + timedelta(days=1) @@ -68,8 +62,11 @@ def __get_intensity(rate): def __get_to(rate): return rate["to"] -def calculate_continuous_times(current_date, target_start_time, target_end_time, target_hours, rates, target_start_offset = None, is_rolling_target = True): - applicable_rates = __get_applicable_rates(current_date, target_start_time, target_end_time, rates, target_start_offset, is_rolling_target) +def calculate_continuous_times(current_date: datetime, target_start_time: str, target_end_time: str, target_hours: float, rates, is_rolling_target = True): + applicable_rates = __get_applicable_rates(current_date, target_start_time, target_end_time, rates, is_rolling_target) + if (applicable_rates is None): + return [] + applicable_rates_count = len(applicable_rates) total_required_rates = math.ceil(target_hours * 2) @@ -101,8 +98,11 @@ def calculate_continuous_times(current_date, target_start_time, target_end_time, return [] -def calculate_intermittent_times(current_date, target_start_time, target_end_time, target_hours, rates, target_start_offset = None, is_rolling_target = True): - applicable_rates = __get_applicable_rates(current_date, target_start_time, target_end_time, rates, target_start_offset, is_rolling_target) +def calculate_intermittent_times(current_date: datetime, target_start_time: str, target_end_time: str, target_hours: float, rates, is_rolling_target = True): + applicable_rates = __get_applicable_rates(current_date, target_start_time, target_end_time, rates, is_rolling_target) + if (applicable_rates is None): + return [] + total_required_rates = math.ceil(target_hours * 2) applicable_rates.sort(key=__get_intensity) diff --git a/custom_components/carbon_intensity/binary_sensors/target_rate.py b/custom_components/carbon_intensity/binary_sensors/target_rate.py index 0bf17d4..f96a5ed 100644 --- a/custom_components/carbon_intensity/binary_sensors/target_rate.py +++ b/custom_components/carbon_intensity/binary_sensors/target_rate.py @@ -112,7 +112,6 @@ def is_on(self): end_time, target_hours, all_rates, - offset, is_rolling_target ) elif (self._config[CONFIG_TARGET_TYPE] == "Intermittent"): @@ -122,7 +121,6 @@ def is_on(self): end_time, target_hours, all_rates, - offset, is_rolling_target ) else: diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index b2cc9aa..3009ee8 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1,5 +1,5 @@ import os -from datetime import timedelta +from datetime import datetime, timedelta def create_rate_data(period_from, period_to, expected_rates: list): rates = [] @@ -23,3 +23,24 @@ def create_rate_data(period_from, period_to, expected_rates: list): rate_index = 0 return rates + +def get_from(rate): + return rate["from"] + +def to_thirty_minute_increments(initial_rates: list): + rates = [] + + for rate in initial_rates: + current_from = datetime.strptime(rate["from"], "%Y-%m-%dT%H:%M:%S%z") + target_to = datetime.strptime(rate["to"], "%Y-%m-%dT%H:%M:%S%z") + while current_from < target_to: + current_to = current_from + timedelta(minutes=30) + rates.append({ + "intensity_forecast": rate["intensity_forecast"], + "from": current_from, + "to": current_to + }) + current_from = current_to + + rates.sort(key=get_from) + return rates \ No newline at end of file diff --git a/tests/unit/test_calculate_continuous_times.py b/tests/unit/test_calculate_continuous_times.py index 6f7cea0..6a4b799 100644 --- a/tests/unit/test_calculate_continuous_times.py +++ b/tests/unit/test_calculate_continuous_times.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta import pytest -from unit import (create_rate_data) +from unit import (create_rate_data, to_thirty_minute_increments) from custom_components.carbon_intensity.binary_sensors import calculate_continuous_times @pytest.mark.asyncio @@ -52,7 +52,6 @@ async def test_when_continuous_times_present_then_next_continuous_times_returned target_end_time, target_hours, rates, - None, is_rolling_target ) @@ -100,25 +99,273 @@ async def test_when_current_time_has_not_enough_time_left_then_no_continuous_tim assert len(result) == 0 @pytest.mark.asyncio -async def test_when_offset_set_then_next_continuous_times_returned_have_offset_applied(): +@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_valid_from,is_rolling_target",[ + (datetime.strptime("2023-01-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2023-01-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False), + (datetime.strptime("2023-01-01T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2023-01-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False), + (datetime.strptime("2023-01-01T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2023-01-01T04:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True), + (datetime.strptime("2023-01-01T23:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, None, True), + + (datetime.strptime("2023-01-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "05:00", "19:00", datetime.strptime("2023-01-01T05:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False), + (datetime.strptime("2023-01-01T06:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), "05:00", "19:00", datetime.strptime("2023-01-01T05:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False), + (datetime.strptime("2023-01-01T06:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), "05:00", "19:00", datetime.strptime("2023-01-01T06:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True), + (datetime.strptime("2023-01-01T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "05:00", "19:00", datetime.strptime("2023-01-01T18:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True), + (datetime.strptime("2023-01-01T18:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), "05:00", "19:00", None, True), + + (datetime.strptime("2023-01-01T20:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "20:00", "06:00", datetime.strptime("2023-01-01T23:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False), + (datetime.strptime("2023-01-02T02:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "20:00", "06:00", datetime.strptime("2023-01-01T23:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False), + (datetime.strptime("2023-01-02T02:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "20:00", "06:00", datetime.strptime("2023-01-02T04:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True), + (datetime.strptime("2023-01-02T05:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), "20:00", "06:00", None, True), +]) +async def test_readme_examples(current_date, target_start_time, target_end_time, expected_first_valid_from, is_rolling_target): # Arrange - period_from = datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - period_to = datetime.strptime("2022-02-11T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - expected_rates = [0.1, 0.2, 0.3, 0.2, 0.2, 0.1] - current_date = datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - target_start_time = "11:00" - target_end_time = "18:00" - offset = "-01:00:00" + rates = to_thirty_minute_increments( + [ + { + "intensity_forecast": 6, + "from": "2023-01-01T00:00:00Z", + "to": "2023-01-01T00:30:00Z" + }, + { + "intensity_forecast": 12, + "from": "2023-01-01T00:30:00Z", + "to": "2023-01-01T05:00:00Z" + }, + { + "intensity_forecast": 7, + "from": "2023-01-01T05:00:00Z", + "to": "2023-01-01T05:30:00Z" + }, + { + "intensity_forecast": 20, + "from": "2023-01-01T05:30:00Z", + "to": "2023-01-01T18:00:00Z" + }, + { + "intensity_forecast": 34, + "from": "2023-01-01T18:00:00Z", + "to": "2023-01-01T23:30:00Z" + }, + { + "intensity_forecast": 5, + "from": "2023-01-01T23:30:00Z", + "to": "2023-01-02T00:30:00Z" + }, + { + "intensity_forecast": 12, + "from": "2023-01-02T00:30:00Z", + "to": "2023-01-02T05:00:00Z" + }, + { + "intensity_forecast": 7, + "from": "2023-01-02T05:00:00Z", + "to": "2023-01-02T05:30:00Z" + }, + { + "intensity_forecast": 20, + "from": "2023-01-02T05:30:00Z", + "to": "2023-01-02T18:00:00Z" + }, + { + "intensity_forecast": 34, + "from": "2023-01-02T18:00:00Z", + "to": "2023-01-02T23:30:00Z" + }, + { + "intensity_forecast": 6, + "from": "2023-01-02T23:30:00Z", + "to": "2023-01-03T00:00:00Z" + }, + ] + ) + + # Restrict our time block + target_hours = 1 - expected_first_valid_from = datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + # Act + result = calculate_continuous_times( + current_date, + target_start_time, + target_end_time, + target_hours, + rates, + is_rolling_target + ) + + # Assert + assert result != None + + if (expected_first_valid_from is None): + assert len(result) == 0 + else: + assert len(result) == 2 + assert result[0]["from"] == expected_first_valid_from + assert result[0]["to"] == expected_first_valid_from + timedelta(minutes=30) + assert result[1]["from"] == expected_first_valid_from + timedelta(minutes=30) + assert result[1]["to"] == expected_first_valid_from + timedelta(minutes=60) + +@pytest.mark.asyncio +async def test_when_last_rate_is_currently_active_and_target_is_rolling_then_rates_are_reevaluated(): + # Arrange + current_date = datetime.strptime("2023-01-01T09:10:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + target_start_time = "09:00" + target_end_time = "22:00" + + expected_first_valid_from = datetime.strptime("2023-01-01T09:30:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + + # Restrict our time block + target_hours = 0.5 + + rates = to_thirty_minute_increments( + [ + { + "intensity_forecast": 6, + "from": "2023-01-01T00:00:00Z", + "to": "2023-01-01T00:30:00Z" + }, + { + "intensity_forecast": 12, + "from": "2023-01-01T00:30:00Z", + "to": "2023-01-01T05:00:00Z" + }, + { + "intensity_forecast": 7, + "from": "2023-01-01T05:00:00Z", + "to": "2023-01-01T05:30:00Z" + }, + { + "intensity_forecast": 20, + "from": "2023-01-01T05:30:00Z", + "to": "2023-01-01T09:30:00Z" + }, + { + "intensity_forecast": 22, + "from": "2023-01-01T09:30:00Z", + "to": "2023-01-01T18:00:00Z" + }, + { + "intensity_forecast": 34, + "from": "2023-01-01T18:00:00Z", + "to": "2023-01-01T23:30:00Z" + }, + { + "intensity_forecast": 5, + "from": "2023-01-01T23:30:00Z", + "to": "2023-01-02T00:30:00Z" + }, + { + "intensity_forecast": 12, + "from": "2023-01-02T00:30:00Z", + "to": "2023-01-02T05:00:00Z" + }, + { + "intensity_forecast": 7, + "from": "2023-01-02T05:00:00Z", + "to": "2023-01-02T05:30:00Z" + }, + { + "intensity_forecast": 20, + "from": "2023-01-02T05:30:00Z", + "to": "2023-01-02T18:00:00Z" + }, + { + "intensity_forecast": 34, + "from": "2023-01-02T18:00:00Z", + "to": "2023-01-02T23:30:00Z" + }, + { + "intensity_forecast": 6, + "from": "2023-01-02T23:30:00Z", + "to": "2023-01-03T00:00:00Z" + }, + ] + ) + + # Act + result = calculate_continuous_times( + current_date, + target_start_time, + target_end_time, + target_hours, + rates + ) + + # Assert + assert result != None + assert len(result) == 1 + assert result[0]["from"] == expected_first_valid_from + assert result[0]["to"] == expected_first_valid_from + timedelta(minutes=30) + assert result[0]["intensity_forecast"] == 22 + +@pytest.mark.asyncio +async def test_when_start_time_is_after_end_time_then_rates_are_overnight(): + # Arrange + current_date = datetime.strptime("2023-01-01T09:10:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + target_start_time = "20:00" + target_end_time = "09:00" + + expected_first_valid_from = datetime.strptime("2023-01-01T23:30:00+00:00", "%Y-%m-%dT%H:%M:%S%z") # Restrict our time block target_hours = 1 - rates = create_rate_data( - period_from, - period_to, - expected_rates + rates = to_thirty_minute_increments( + [ + { + "intensity_forecast": 6, + "from": "2023-01-01T00:00:00Z", + "to": "2023-01-01T00:30:00Z" + }, + { + "intensity_forecast": 12, + "from": "2023-01-01T00:30:00Z", + "to": "2023-01-01T05:00:00Z" + }, + { + "intensity_forecast": 7, + "from": "2023-01-01T05:00:00Z", + "to": "2023-01-01T05:30:00Z" + }, + { + "intensity_forecast": 20, + "from": "2023-01-01T05:30:00Z", + "to": "2023-01-01T18:00:00Z" + }, + { + "intensity_forecast": 34, + "from": "2023-01-01T18:00:00Z", + "to": "2023-01-01T23:30:00Z" + }, + { + "intensity_forecast": 5, + "from": "2023-01-01T23:30:00Z", + "to": "2023-01-02T00:30:00Z" + }, + { + "intensity_forecast": 12, + "from": "2023-01-02T00:30:00Z", + "to": "2023-01-02T05:00:00Z" + }, + { + "intensity_forecast": 7, + "from": "2023-01-02T05:00:00Z", + "to": "2023-01-02T05:30:00Z" + }, + { + "intensity_forecast": 20, + "from": "2023-01-02T05:30:00Z", + "to": "2023-01-02T18:00:00Z" + }, + { + "intensity_forecast": 34, + "from": "2023-01-02T18:00:00Z", + "to": "2023-01-02T23:30:00Z" + }, + { + "intensity_forecast": 6, + "from": "2023-01-02T23:30:00Z", + "to": "2023-01-03T00:00:00Z" + }, + ] ) # Act @@ -128,7 +375,7 @@ async def test_when_offset_set_then_next_continuous_times_returned_have_offset_a target_end_time, target_hours, rates, - offset + False ) # Assert @@ -136,8 +383,258 @@ async def test_when_offset_set_then_next_continuous_times_returned_have_offset_a assert len(result) == 2 assert result[0]["from"] == expected_first_valid_from assert result[0]["to"] == expected_first_valid_from + timedelta(minutes=30) - assert result[0]["intensity_forecast"] == 0.1 + assert result[0]["intensity_forecast"] == 5 assert result[1]["from"] == expected_first_valid_from + timedelta(minutes=30) - assert result[1]["to"] == expected_first_valid_from + timedelta(hours=1) - assert result[1]["intensity_forecast"] == 0.1 \ No newline at end of file + assert result[1]["to"] == expected_first_valid_from + timedelta(minutes=60) + assert result[1]["intensity_forecast"] == 5 + +@pytest.mark.asyncio +async def test_when_start_time_and_end_time_is_same_then_rates_are_shifted(): + # Arrange + current_date = datetime.strptime("2023-01-01T17:10:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + target_start_time = "16:00" + target_end_time = "16:00" + + expected_first_valid_from = datetime.strptime("2023-01-01T23:30:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + + # Restrict our time block + target_hours = 1 + + rates = to_thirty_minute_increments( + [ + { + "intensity_forecast": 6, + "from": "2023-01-01T00:00:00Z", + "to": "2023-01-01T00:30:00Z" + }, + { + "intensity_forecast": 12, + "from": "2023-01-01T00:30:00Z", + "to": "2023-01-01T05:00:00Z" + }, + { + "intensity_forecast": 7, + "from": "2023-01-01T05:00:00Z", + "to": "2023-01-01T05:30:00Z" + }, + { + "intensity_forecast": 20, + "from": "2023-01-01T05:30:00Z", + "to": "2023-01-01T18:00:00Z" + }, + { + "intensity_forecast": 34, + "from": "2023-01-01T18:00:00Z", + "to": "2023-01-01T23:30:00Z" + }, + { + "intensity_forecast": 5, + "from": "2023-01-01T23:30:00Z", + "to": "2023-01-02T00:30:00Z" + }, + { + "intensity_forecast": 12, + "from": "2023-01-02T00:30:00Z", + "to": "2023-01-02T05:00:00Z" + }, + { + "intensity_forecast": 7, + "from": "2023-01-02T05:00:00Z", + "to": "2023-01-02T05:30:00Z" + }, + { + "intensity_forecast": 20, + "from": "2023-01-02T05:30:00Z", + "to": "2023-01-02T18:00:00Z" + }, + { + "intensity_forecast": 34, + "from": "2023-01-02T18:00:00Z", + "to": "2023-01-02T23:30:00Z" + }, + { + "intensity_forecast": 6, + "from": "2023-01-02T23:30:00Z", + "to": "2023-01-03T00:00:00Z" + }, + ] + ) + + # Act + result = calculate_continuous_times( + current_date, + target_start_time, + target_end_time, + target_hours, + rates, + False + ) + + # Assert + assert result != None + assert len(result) == 2 + assert result[0]["from"] == expected_first_valid_from + assert result[0]["to"] == expected_first_valid_from + timedelta(minutes=30) + assert result[0]["intensity_forecast"] == 5 + + assert result[1]["from"] == expected_first_valid_from + timedelta(minutes=30) + assert result[1]["to"] == expected_first_valid_from + timedelta(minutes=60) + assert result[1]["intensity_forecast"] == 5 + + +@pytest.mark.asyncio +async def test_when_start_time_is_after_end_time_and_rolling_target_then_rates_are_overnight(): + # Arrange + current_date = datetime.strptime("2022-10-21T23:30:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + target_start_time = "22:00" + target_end_time = "01:00" + + expected_first_valid_from = datetime.strptime("2022-10-21T23:30:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + + # Restrict our time block + target_hours = 1 + + rates = to_thirty_minute_increments( + [ + { + "intensity_forecast": 15.1, + "from": "2022-10-21T00:00:00Z", + "to": "2022-10-21T22:00:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-21T22:00:00Z", + "to": "2022-10-22T02:00:00Z" + }, + { + "intensity_forecast": 15.1, + "from": "2022-10-22T02:00:00Z", + "to": "2022-10-22T05:00:00Z" + }, + ] + ) + + # Act + result = calculate_continuous_times( + current_date, + target_start_time, + target_end_time, + target_hours, + rates, + True + ) + + # Assert + assert result != None + assert len(result) == 2 + assert result[0]["from"] == expected_first_valid_from + assert result[0]["to"] == expected_first_valid_from + timedelta(minutes=30) + assert result[0]["intensity_forecast"] == 16.1 + + assert result[1]["from"] == expected_first_valid_from + timedelta(minutes=30) + assert result[1]["to"] == expected_first_valid_from + timedelta(minutes=60) + assert result[1]["intensity_forecast"] == 16.1 + +@pytest.mark.asyncio +async def test_when_start_time_and_end_time_is_same_and_rolling_target_then_rates_are_shifted(): + # Arrange + current_date = datetime.strptime("2022-10-21T23:30:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + target_start_time = "16:00" + target_end_time = "16:00" + + expected_first_valid_from = datetime.strptime("2022-10-22T02:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + + # Restrict our time block + target_hours = 1 + + rates = to_thirty_minute_increments( + [ + { + "intensity_forecast": 15.1, + "from": "2022-10-21T00:00:00Z", + "to": "2022-10-21T22:00:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-21T22:00:00Z", + "to": "2022-10-22T02:00:00Z" + }, + { + "intensity_forecast": 15.1, + "from": "2022-10-22T02:00:00Z", + "to": "2022-10-22T05:00:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-22T05:00:00Z", + "to": "2022-10-23T00:00:00Z" + }, + ] + ) + + # Act + result = calculate_continuous_times( + current_date, + target_start_time, + target_end_time, + target_hours, + rates, + True + ) + + # Assert + assert result != None + assert len(result) == 2 + assert result[0]["from"] == expected_first_valid_from + assert result[0]["to"] == expected_first_valid_from + timedelta(minutes=30) + assert result[0]["intensity_forecast"] == 15.1 + +@pytest.mark.asyncio +async def test_when_available_rates_are_too_low_then_no_times_are_returned(): + # Arrange + current_date = datetime.strptime("2022-10-22T22:40:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + target_start_time = "16:00" + target_end_time = "16:00" + + # Restrict our time block + target_hours = 3 + + rates = to_thirty_minute_increments( + [ + { + "intensity_forecast": 15.1, + "from": "2022-10-21T00:00:00Z", + "to": "2022-10-21T22:00:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-21T22:00:00Z", + "to": "2022-10-22T02:00:00Z" + }, + { + "intensity_forecast": 15.1, + "from": "2022-10-22T02:00:00Z", + "to": "2022-10-22T05:00:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-22T05:00:00Z", + "to": "2022-10-23T00:00:00Z" + }, + ] + ) + + # Act + result = calculate_continuous_times( + current_date, + target_start_time, + target_end_time, + target_hours, + rates, + False + ) + + # Assert + assert result != None + assert len(result) == 0 \ No newline at end of file diff --git a/tests/unit/test_calculate_intermittent_times.py b/tests/unit/test_calculate_intermittent_times.py index 81dec6a..fc8748a 100644 --- a/tests/unit/test_calculate_intermittent_times.py +++ b/tests/unit/test_calculate_intermittent_times.py @@ -1,11 +1,11 @@ from datetime import datetime, timedelta import pytest -from unit import (create_rate_data) +from unit import (create_rate_data, to_thirty_minute_increments) from custom_components.carbon_intensity.binary_sensors import calculate_intermittent_times @pytest.mark.asyncio -@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_valid_from,is_rolling_target",[ +@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_from,is_rolling_target",[ (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True), (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True), (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True), @@ -30,7 +30,7 @@ (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False), (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False), ]) -async def test_when_intermittent_times_present_then_next_intermittent_times_returned(current_date, target_start_time, target_end_time, expected_first_valid_from, is_rolling_target): +async def test_when_intermittent_times_present_then_next_intermittent_times_returned(current_date, target_start_time, target_end_time, expected_first_from, is_rolling_target): # Arrange period_from = datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") period_to = datetime.strptime("2022-02-11T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -52,19 +52,18 @@ async def test_when_intermittent_times_present_then_next_intermittent_times_retu target_end_time, target_hours, rates, - None, is_rolling_target ) # Assert assert result != None assert len(result) == 2 - assert result[0]["from"] == expected_first_valid_from - assert result[0]["to"] == expected_first_valid_from + timedelta(minutes=30) + assert result[0]["from"] == expected_first_from + assert result[0]["to"] == expected_first_from + timedelta(minutes=30) assert result[0]["intensity_forecast"] == 0.1 - assert result[1]["from"] == expected_first_valid_from + timedelta(hours=1, minutes=30) - assert result[1]["to"] == expected_first_valid_from + timedelta(hours=2) + assert result[1]["from"] == expected_first_from + timedelta(hours=1, minutes=30) + assert result[1]["to"] == expected_first_from + timedelta(hours=2) assert result[1]["intensity_forecast"] == 0.1 @pytest.mark.asyncio @@ -100,23 +99,102 @@ async def test_when_current_time_has_not_enough_time_left_then_no_intermittent_t assert len(result) == 0 @pytest.mark.asyncio -async def test_when_offset_set_then_next_continuous_times_returned_have_offset_applied(): +async def test_when_start_time_is_after_end_time_then_rates_are_overnight(): # Arrange - period_from = datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - period_to = datetime.strptime("2022-02-11T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - expected_rates = [0.1, 0.2, 0.3] - current_date = datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - target_start_time = "11:00" - target_end_time = "18:00" - offset = "-01:00:00" + current_date = datetime.strptime("2022-10-21T09:10:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + target_start_time = "20:00" + target_end_time = "09:00" # Restrict our time block target_hours = 1 - rates = create_rate_data( - period_from, - period_to, - expected_rates + rates = to_thirty_minute_increments([ + { + "intensity_forecast": 15.1, + "from": "2022-10-21T00:00:00Z", + "to": "2022-10-21T22:00:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-21T22:00:00Z", + "to": "2022-10-22T02:00:00Z" + }, + { + "intensity_forecast": 14.1, + "from": "2022-10-22T02:00:00Z", + "to": "2022-10-22T02:30:00Z" + }, + { + "intensity_forecast": 15.1, + "from": "2022-10-22T02:30:00Z", + "to": "2022-10-22T09:00:00Z" + }, + ]) + + # Act + result = calculate_intermittent_times( + current_date, + target_start_time, + target_end_time, + target_hours, + rates, + False + ) + + # Assert + assert result != None + assert len(result) == 2 + assert result[0]["from"] == datetime.strptime("2022-10-21T20:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[0]["to"] == datetime.strptime("2022-10-21T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[0]["intensity_forecast"] == 15.1 + + assert result[1]["from"] == datetime.strptime("2022-10-22T02:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[1]["to"] == datetime.strptime("2022-10-22T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[1]["intensity_forecast"] == 14.1 + +@pytest.mark.asyncio +async def test_when_start_time_and_end_time_is_same_then_rates_are_shifted(): + # Arrange + current_date = datetime.strptime("2022-10-21T17:10:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + target_start_time = "16:00" + target_end_time = "16:00" + + # Restrict our time block + target_hours = 1 + + rates = to_thirty_minute_increments( + [ + { + "intensity_forecast": 15.1, + "from": "2022-10-21T00:00:00Z", + "to": "2022-10-21T22:00:00Z" + }, + { + "intensity_forecast": 14.1, + "from": "2022-10-21T22:00:00Z", + "to": "2022-10-21T22:30:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-21T22:30:00Z", + "to": "2022-10-22T02:00:00Z" + }, + { + "intensity_forecast": 14.1, + "from": "2022-10-22T02:00:00Z", + "to": "2022-10-22T02:30:00Z" + }, + { + "intensity_forecast": 15.1, + "from": "2022-10-22T02:30:00Z", + "to": "2022-10-22T05:00:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-22T05:00:00Z", + "to": "2022-10-23T00:00:00Z" + }, + ] ) # Act @@ -126,16 +204,203 @@ async def test_when_offset_set_then_next_continuous_times_returned_have_offset_a target_end_time, target_hours, rates, - offset + False ) # Assert assert result != None assert len(result) == 2 - assert result[0]["from"] == datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - assert result[0]["to"] == datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + timedelta(minutes=30) - assert result[0]["intensity_forecast"] == 0.1 + assert result[0]["from"] == datetime.strptime("2022-10-21T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[0]["to"] == datetime.strptime("2022-10-21T22:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[0]["intensity_forecast"] == 14.1 + + assert result[1]["from"] == datetime.strptime("2022-10-22T02:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[1]["to"] == datetime.strptime("2022-10-22T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[1]["intensity_forecast"] == 14.1 + +@pytest.mark.asyncio +async def test_when_start_time_is_after_end_time_and_rolling_target_then_rates_are_overnight(): + # Arrange + current_date = datetime.strptime("2022-10-21T21:10:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + target_start_time = "20:00" + target_end_time = "09:00" + + # Restrict our time block + target_hours = 1 + + rates = to_thirty_minute_increments( + [ + { + "intensity_forecast": 15.1, + "from": "2022-10-21T00:00:00Z", + "to": "2022-10-21T22:00:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-21T22:00:00Z", + "to": "2022-10-22T02:00:00Z" + }, + { + "intensity_forecast": 14.1, + "from": "2022-10-22T02:00:00Z", + "to": "2022-10-22T02:30:00Z" + }, + { + "intensity_forecast": 15.1, + "from": "2022-10-22T02:30:00Z", + "to": "2022-10-22T05:00:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-22T05:00:00Z", + "to": "2022-10-23T00:00:00Z" + }, + ] + ) + + # Act + result = calculate_intermittent_times( + current_date, + target_start_time, + target_end_time, + target_hours, + rates, + True + ) + + # Assert + assert result != None + assert len(result) == 2 + assert result[0]["from"] == datetime.strptime("2022-10-21T21:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[0]["to"] == datetime.strptime("2022-10-21T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[0]["intensity_forecast"] == 15.1 + + assert result[1]["from"] == datetime.strptime("2022-10-22T02:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[1]["to"] == datetime.strptime("2022-10-22T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[1]["intensity_forecast"] == 14.1 + +@pytest.mark.asyncio +async def test_when_start_time_and_end_time_is_same_and_rolling_target_then_rates_are_shifted(): + # Arrange + current_date = datetime.strptime("2022-10-21T22:40:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + target_start_time = "16:00" + target_end_time = "16:00" + + # Restrict our time block + target_hours = 1 - assert result[1]["from"] == datetime.strptime("2022-02-09T13:30:00Z", "%Y-%m-%dT%H:%M:%S%z") - assert result[1]["to"] == datetime.strptime("2022-02-09T13:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + timedelta(minutes=30) - assert result[1]["intensity_forecast"] == 0.1 \ No newline at end of file + rates = to_thirty_minute_increments( + [ + { + "intensity_forecast": 15.1, + "from": "2022-10-21T00:00:00Z", + "to": "2022-10-21T22:00:00Z" + }, + { + "intensity_forecast": 14.1, + "from": "2022-10-21T22:00:00Z", + "to": "2022-10-21T22:30:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-21T22:30:00Z", + "to": "2022-10-22T02:00:00Z" + }, + { + "intensity_forecast": 14.1, + "from": "2022-10-22T02:00:00Z", + "to": "2022-10-22T02:30:00Z" + }, + { + "intensity_forecast": 15.1, + "from": "2022-10-22T02:30:00Z", + "to": "2022-10-22T05:00:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-22T05:00:00Z", + "to": "2022-10-23T00:00:00Z" + }, + ] + ) + + # Act + result = calculate_intermittent_times( + current_date, + target_start_time, + target_end_time, + target_hours, + rates, + True + ) + + # Assert + assert result != None + assert len(result) == 2 + + assert result[0]["from"] == datetime.strptime("2022-10-22T02:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[0]["to"] == datetime.strptime("2022-10-22T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[0]["intensity_forecast"] == 14.1 + + assert result[1]["from"] == datetime.strptime("2022-10-22T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[1]["to"] == datetime.strptime("2022-10-22T03:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert result[1]["intensity_forecast"] == 15.1 + +@pytest.mark.asyncio +async def test_when_available_rates_are_too_low_then_no_times_are_returned(): + # Arrange + current_date = datetime.strptime("2022-10-22T22:40:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + target_start_time = "16:00" + target_end_time = "16:00" + + # Restrict our time block + target_hours = 3 + + rates = to_thirty_minute_increments( + [ + { + "intensity_forecast": 15.1, + "from": "2022-10-21T00:00:00Z", + "to": "2022-10-21T22:00:00Z" + }, + { + "intensity_forecast": 14.1, + "from": "2022-10-21T22:00:00Z", + "to": "2022-10-21T22:30:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-21T22:30:00Z", + "to": "2022-10-22T02:00:00Z" + }, + { + "intensity_forecast": 14.1, + "from": "2022-10-22T02:00:00Z", + "to": "2022-10-22T02:30:00Z" + }, + { + "intensity_forecast": 15.1, + "from": "2022-10-22T02:30:00Z", + "to": "2022-10-22T05:00:00Z" + }, + { + "intensity_forecast": 16.1, + "from": "2022-10-22T05:00:00Z", + "to": "2022-10-23T00:00:00Z" + }, + ] + ) + + # Act + result = calculate_intermittent_times( + current_date, + target_start_time, + target_end_time, + target_hours, + rates, + False + ) + + # Assert + assert result != None + assert len(result) == 0 \ No newline at end of file