diff --git a/_docs/faq.md b/_docs/faq.md index 18191ce..12c952d 100644 --- a/_docs/faq.md +++ b/_docs/faq.md @@ -4,6 +4,22 @@ For some reason when you update the configuration via the integration page, the associated entities don't update. You'll need to reload the parent entry to get the configuration to take effect. This is something I'm currently investigating. +## I've setup a target timeframe with the default time period (00:00-00:00) or a rolling target timeframe looking ahead for 24 hours but it's not updating. Is something broken? + +By default, the target timeframe sensors require the supporting data for the specified time periods to be available in order to be calculate. For example if it was `00:00` on `1/12/2025`, then the standard target timeframe would require data for _at least_ between `2025-12-01T00:00` and `2025-12-01T00:00`. If this is not the case, then the sensor will not be evaluated. This is made clearer by the `values_incomplete` attributes of the [target timeframe](./setup/target_timeframe.md#attributes) and [rolling target timeframe](./setup/rolling_target_timeframe.md#attributes). + +For some data sources, this might cause issues due to the data available (e.g. When you're on the Agile tariff of [Octopus Energy UK](./blueprints.md#octopus-energy) where data is available in advanced up to `23:00`). + +In this scenario, you have two options. + +1. The recommended approach would be to adjust the time period that the target timeframe looks at. See below for example suggestions + +| Data Source | Standard Target Timeframe Recommendation | Rolling Target Timeframe Recommendation | +|-|-|-| +| Agile tariff for [Octopus Energy UK](./blueprints.md#octopus-energy) | Have an end time before or equal to `23:00` (e.g. `23:00-23:00` if you want to look at a full 24 hours) | Because data refreshes around `16:00` and will go up to `23:00`, then your look ahead hours should be no more than `7` to ensure it's working `99%` of the time | + +2. Set the configuration option to [calculate with incomplete data](./setup/target_timeframe.md#calculate-with-incomplete-data). This _could_ have undesired consequences in the calculations (e.g. picking times that look odd retrospectively because the full data wasn't available at the time of picking), so use with caution. + ## How do I increase the logs for the integration? If you are having issues, it would be helpful to include Home Assistant logs as part of any raised issue. This can be done by following the [instructions](https://www.home-assistant.io/docs/configuration/troubleshooting/#enabling-debug-logging) outlined by Home Assistant. diff --git a/_docs/setup/rolling_target_timeframe.md b/_docs/setup/rolling_target_timeframe.md index 2a5b45c..e49fd55 100644 --- a/_docs/setup/rolling_target_timeframe.md +++ b/_docs/setup/rolling_target_timeframe.md @@ -96,22 +96,34 @@ There may be times that you want the target timeframe sensors to not take into a If hours mode is set to **minimum**, then a minimum and/or maximum rate must be specified in order for the target timeframe sensor to know what the cut off is for discovered times. -### Weighting +### Weighting/Multipliers !!! info - This is only available for **continuous** target timeframe sensors in **exact** hours mode. + This is only available for **continuous** target value sensors in **exact** hours mode. -There may be times when the device you're wanting the target timeframe sensor to turn on doesn't have a consistent power draw. You can specify a weighting which can be applied to each discovered 30 minute slot. This can be specified in a few different ways. Take the following example weighting for a required 2 hours. +There may be times when the device you're wanting the target value sensor to turn on doesn't have a consistent power draw. You can specify a weighting/multiplier which can be applied to the value of each discovered 30 minute slot. This can be specified in a few different ways. Take the following example weighting/multiplier for a required 2 hours. -* `1,1,2,1` - This applies a weighting of 1 to the first, second and forth slot and a weighting of 2 to the third slot. This will try and make the lowest slot fall on the third slot, as long as the surrounding slots are cheaper than other continuous slots. -* `*,2,1` - This applies a weighting of 1 to the first, second and forth slot and a weighting of 2 to the third slot. The `*` can be used as a placeholder for the standard weighting of 1 for all slots before the ones specified. -* `1,1,2,*` - This applies a weighting of 1 to the first, second and forth slot and a weighting of 2 to the third slot. The `*` can be used as a placeholder for the standard weighting of 1 for all slots after the ones specified. -* `2,*,2` - This applies a weighting of 2 to the first and forth slot and a weighting of 1 to all slots in between. The `*` can be used as a placeholder for the standard weighting of 1 for all slots in between the specified slots. +* `1,1,2,1` - This applies a weighting/multiplier of 1 to the first, second and forth slot and a weighting/multiplier of 2 to the third slot. This will try and make the lowest slot fall on the third slot, as long as the surrounding slots are cheaper than other continuous slots. +* `*,2,1` - This applies a weighting/multiplier of 1 to the first, second and forth slot and a weighting/multiplier of 2 to the third slot. The `*` can be used as a placeholder for the standard weighting/multiplier of 1 for all slots before the ones specified. +* `1,1,2,*` - This applies a weighting/multiplier of 1 to the first, second and forth slot and a weighting/multiplier of 2 to the third slot. The `*` can be used as a placeholder for the standard weighting/multiplier of 1 for all slots after the ones specified. +* `2,*,2` - This applies a weighting/multiplier of 2 to the first and forth slot and a weighting/multiplier of 1 to all slots in between. The `*` can be used as a placeholder for the standard weighting/multiplier of 1 for all slots in between the specified slots. -Each slot weighting must be a whole number or decimal number and be positive. +Each slot weighting/multiplier must be a whole number or decimal number and be positive. -You can also use weightings to ignore slots. This can be done by assigning a value of 0 for the desired slot. +You can also use weightings/multipliers to ignore slots. This can be done by assigning a value of 0 for the desired slot. + +### Dangerous settings + +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. + +#### Calculate with incomplete data + +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). + +#### Minimum required minutes in slots + +By default, 30 minute slots that are part way through are not considered when evaluating rolling target time frames. For example, if you are looking for the best slots for the next 4 hours and it's 10:01, then only slots between 10:30 to 14:30 will be evaluated. This threshold can be changed here to a lower value if you want to take account of slots that are partially in the past. For example if this was set to 29, then the previous example would evaluate slots between 10:00 to 14:00. ## Attributes @@ -141,6 +153,7 @@ The following attributes are available on each sensor | `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. | | `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. | | `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. | +| `calculate_with_incomplete_data` | boolean | Determines if calculations should occur when there isn't enough data to satisfy the look ahead hours | ## Services diff --git a/_docs/setup/target_timeframe.md b/_docs/setup/target_timeframe.md index ca99904..ade5ff0 100644 --- a/_docs/setup/target_timeframe.md +++ b/_docs/setup/target_timeframe.md @@ -106,22 +106,38 @@ There may be times that you want the target timeframe sensors to not take into a If hours mode is set to **minimum**, then a minimum and/or maximum value must be specified in order for the target timeframe sensor to know what the cut off is for discovered times. -### Weighting +### Weighting/Multipliers !!! info This is only available for **continuous** target value sensors in **exact** hours mode. -There may be times when the device you're wanting the target value sensor to turn on doesn't have a consistent power draw. You can specify a weighting which can be applied to each discovered 30 minute slot. This can be specified in a few different ways. Take the following example weighting for a required 2 hours. +There may be times when the device you're wanting the target value sensor to turn on doesn't have a consistent power draw. You can specify a weighting/multiplier which can be applied to the value of each discovered 30 minute slot. This can be specified in a few different ways. Take the following example weighting/multiplier for a required 2 hours. -* `1,1,2,1` - This applies a weighting of 1 to the first, second and forth slot and a weighting of 2 to the third slot. This will try and make the lowest slot fall on the third slot, as long as the surrounding slots are cheaper than other continuous slots. -* `*,2,1` - This applies a weighting of 1 to the first, second and forth slot and a weighting of 2 to the third slot. The `*` can be used as a placeholder for the standard weighting of 1 for all slots before the ones specified. -* `1,1,2,*` - This applies a weighting of 1 to the first, second and forth slot and a weighting of 2 to the third slot. The `*` can be used as a placeholder for the standard weighting of 1 for all slots after the ones specified. -* `2,*,2` - This applies a weighting of 2 to the first and forth slot and a weighting of 1 to all slots in between. The `*` can be used as a placeholder for the standard weighting of 1 for all slots in between the specified slots. +* `1,1,2,1` - This applies a weighting/multiplier of 1 to the first, second and forth slot and a weighting/multiplier of 2 to the third slot. This will try and make the lowest slot fall on the third slot, as long as the surrounding slots are cheaper than other continuous slots. +* `*,2,1` - This applies a weighting/multiplier of 1 to the first, second and forth slot and a weighting/multiplier of 2 to the third slot. The `*` can be used as a placeholder for the standard weighting/multiplier of 1 for all slots before the ones specified. +* `1,1,2,*` - This applies a weighting/multiplier of 1 to the first, second and forth slot and a weighting/multiplier of 2 to the third slot. The `*` can be used as a placeholder for the standard weighting/multiplier of 1 for all slots after the ones specified. +* `2,*,2` - This applies a weighting/multiplier of 2 to the first and forth slot and a weighting/multiplier of 1 to all slots in between. The `*` can be used as a placeholder for the standard weighting/multiplier of 1 for all slots in between the specified slots. -Each slot weighting must be a whole number or decimal number and be positive. +Each slot weighting/multiplier must be a whole number or decimal number and be positive. -You can also use weightings to ignore slots. This can be done by assigning a value of 0 for the desired slot. +You can also use weightings/multipliers to ignore slots. This can be done by assigning a value of 0 for the desired slot. + +### Dangerous settings + +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. + +#### Calculate with incomplete data + +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). + +#### Minimum required minutes in slots + +By default, 30 minute slots that are part way through are not considered when evaluating target time frames. For example, if you are looking for the best slots between 10:00 to 12:00 and it's 10:01, then only slots between 10:30 to 12:00 will be evaluated. This threshold can be changed here to a lower value if you want to take account of slots that are partially in the past. For example if this was set to 29, then the previous example would evaluate slots between 10:00 to 12:00. + +!!! warn + + Changing this can cause sensors to not come on for the correct amount of time by up to 30 minutes. ## Attributes @@ -154,6 +170,8 @@ The following attributes are available on each sensor | `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. | | `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. | | `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. | +| `calculate_with_incomplete_data` | boolean | Determines if calculations should occur when there isn't enough data to satisfy the look ahead hours | +| `minimum_required_minutes_in_slot` | integer | Determines the configured minimum number of minutes to be present in a slot for it to be considered | ## Services diff --git a/custom_components/target_timeframes/config/rolling_target_timeframe.py b/custom_components/target_timeframes/config/rolling_target_timeframe.py index ee7deb5..7fdfa0a 100644 --- a/custom_components/target_timeframes/config/rolling_target_timeframe.py +++ b/custom_components/target_timeframes/config/rolling_target_timeframe.py @@ -2,12 +2,14 @@ from ..const import ( CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD, + CONFIG_TARGET_DANGEROUS_SETTINGS, CONFIG_TARGET_HOURS, CONFIG_TARGET_HOURS_MODE, CONFIG_TARGET_HOURS_MODE_EXACT, CONFIG_TARGET_HOURS_MODE_MINIMUM, CONFIG_TARGET_MAX_VALUE, CONFIG_TARGET_MIN_VALUE, + CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT, CONFIG_TARGET_NAME, CONFIG_TARGET_OFFSET, CONFIG_TARGET_TYPE, @@ -15,6 +17,7 @@ CONFIG_TARGET_WEIGHTING, REGEX_ENTITY_NAME, REGEX_HOURS, + REGEX_INTEGER, REGEX_OFFSET_PARTS, REGEX_VALUE, REGEX_WEIGHTING @@ -81,21 +84,32 @@ def validate_rolling_target_timeframe_config(data): if matches is None: errors[CONFIG_TARGET_OFFSET] = "invalid_offset" + minimum_value: float | None = None if CONFIG_TARGET_MIN_VALUE in data and data[CONFIG_TARGET_MIN_VALUE] is not None: if isinstance(data[CONFIG_TARGET_MIN_VALUE], float) == False: matches = re.search(REGEX_VALUE, data[CONFIG_TARGET_MIN_VALUE]) if matches is None: errors[CONFIG_TARGET_MIN_VALUE] = "invalid_value" else: - data[CONFIG_TARGET_MIN_VALUE] = float(data[CONFIG_TARGET_MIN_VALUE]) + minimum_value = float(data[CONFIG_TARGET_MIN_VALUE]) + data[CONFIG_TARGET_MIN_VALUE] = minimum_value + else: + minimum_value = data[CONFIG_TARGET_MIN_VALUE] + maximum_value: float | None = None if CONFIG_TARGET_MAX_VALUE in data and data[CONFIG_TARGET_MAX_VALUE] is not None: if isinstance(data[CONFIG_TARGET_MAX_VALUE], float) == False: matches = re.search(REGEX_VALUE, data[CONFIG_TARGET_MAX_VALUE]) if matches is None: errors[CONFIG_TARGET_MAX_VALUE] = "invalid_value" else: - data[CONFIG_TARGET_MAX_VALUE] = float(data[CONFIG_TARGET_MAX_VALUE]) + maximum_value = float(data[CONFIG_TARGET_MAX_VALUE]) + data[CONFIG_TARGET_MAX_VALUE] = maximum_value + else: + maximum_value = data[CONFIG_TARGET_MAX_VALUE] + + if minimum_value is not None and maximum_value is not None and minimum_value > maximum_value: + errors[CONFIG_TARGET_MIN_VALUE] = "minimum_value_not_less_than_maximum_value" if CONFIG_TARGET_WEIGHTING in data and data[CONFIG_TARGET_WEIGHTING] is not None: matches = re.search(REGEX_WEIGHTING, data[CONFIG_TARGET_WEIGHTING]) @@ -122,4 +136,25 @@ def validate_rolling_target_timeframe_config(data): if CONFIG_TARGET_HOURS not in errors and CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD not in errors and data[CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD] < data[CONFIG_TARGET_HOURS]: errors[CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD] = "look_ahead_hours_not_long_enough" + minimum_required_minutes_in_slot: int | None = None + if (CONFIG_TARGET_DANGEROUS_SETTINGS in data and + CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT in data[CONFIG_TARGET_DANGEROUS_SETTINGS] and + data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] is not None): + + if isinstance(data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT], int) == False: + if isinstance(data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT], str): + matches = re.search(REGEX_INTEGER, data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT]) + if matches is None: + errors[CONFIG_TARGET_DANGEROUS_SETTINGS] = "invalid_integer" + else: + minimum_required_minutes_in_slot = int(data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT]) + data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] = minimum_required_minutes_in_slot + else: + errors[CONFIG_TARGET_DANGEROUS_SETTINGS] = "invalid_integer" + else: + minimum_required_minutes_in_slot = data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] + + if minimum_required_minutes_in_slot is not None and (minimum_required_minutes_in_slot < 1 or minimum_required_minutes_in_slot > 30): + errors[CONFIG_TARGET_DANGEROUS_SETTINGS] = "invalid_minimum_required_minutes_in_slot" + return errors \ No newline at end of file diff --git a/custom_components/target_timeframes/config/target_timeframe.py b/custom_components/target_timeframes/config/target_timeframe.py index f1ede25..379c65c 100644 --- a/custom_components/target_timeframes/config/target_timeframe.py +++ b/custom_components/target_timeframes/config/target_timeframe.py @@ -4,6 +4,7 @@ from homeassistant.util.dt import (parse_datetime) from ..const import ( + CONFIG_TARGET_DANGEROUS_SETTINGS, CONFIG_TARGET_END_TIME, CONFIG_TARGET_HOURS, CONFIG_TARGET_HOURS_MODE, @@ -11,6 +12,7 @@ CONFIG_TARGET_HOURS_MODE_MINIMUM, CONFIG_TARGET_MAX_VALUE, CONFIG_TARGET_MIN_VALUE, + CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT, CONFIG_TARGET_NAME, CONFIG_TARGET_OFFSET, CONFIG_TARGET_START_TIME, @@ -19,6 +21,7 @@ CONFIG_TARGET_WEIGHTING, REGEX_ENTITY_NAME, REGEX_HOURS, + REGEX_INTEGER, REGEX_OFFSET_PARTS, REGEX_VALUE, REGEX_TIME, @@ -104,21 +107,32 @@ def validate_target_timeframe_config(data): if matches is None: errors[CONFIG_TARGET_OFFSET] = "invalid_offset" + minimum_value: float | None = None if CONFIG_TARGET_MIN_VALUE in data and data[CONFIG_TARGET_MIN_VALUE] is not None: if isinstance(data[CONFIG_TARGET_MIN_VALUE], float) == False: matches = re.search(REGEX_VALUE, data[CONFIG_TARGET_MIN_VALUE]) if matches is None: errors[CONFIG_TARGET_MIN_VALUE] = "invalid_value" else: - data[CONFIG_TARGET_MIN_VALUE] = float(data[CONFIG_TARGET_MIN_VALUE]) + minimum_value = float(data[CONFIG_TARGET_MIN_VALUE]) + data[CONFIG_TARGET_MIN_VALUE] = minimum_value + else: + minimum_value = data[CONFIG_TARGET_MIN_VALUE] + maximum_value: float | None = None if CONFIG_TARGET_MAX_VALUE in data and data[CONFIG_TARGET_MAX_VALUE] is not None: if isinstance(data[CONFIG_TARGET_MAX_VALUE], float) == False: matches = re.search(REGEX_VALUE, data[CONFIG_TARGET_MAX_VALUE]) if matches is None: errors[CONFIG_TARGET_MAX_VALUE] = "invalid_value" else: - data[CONFIG_TARGET_MAX_VALUE] = float(data[CONFIG_TARGET_MAX_VALUE]) + maximum_value = float(data[CONFIG_TARGET_MAX_VALUE]) + data[CONFIG_TARGET_MAX_VALUE] = maximum_value + else: + maximum_value = data[CONFIG_TARGET_MAX_VALUE] + + if minimum_value is not None and maximum_value is not None and minimum_value > maximum_value: + errors[CONFIG_TARGET_MIN_VALUE] = "minimum_value_not_less_than_maximum_value" if CONFIG_TARGET_WEIGHTING in data and data[CONFIG_TARGET_WEIGHTING] is not None: matches = re.search(REGEX_WEIGHTING, data[CONFIG_TARGET_WEIGHTING]) @@ -151,4 +165,25 @@ def validate_target_timeframe_config(data): if is_time_frame_long_enough(data[CONFIG_TARGET_HOURS], start_time, end_time) == False: errors[CONFIG_TARGET_HOURS] = "invalid_hours_time_frame" + minimum_required_minutes_in_slot: int | None = None + if (CONFIG_TARGET_DANGEROUS_SETTINGS in data and + CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT in data[CONFIG_TARGET_DANGEROUS_SETTINGS] and + data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] is not None): + + if isinstance(data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT], int) == False: + if isinstance(data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT], str): + matches = re.search(REGEX_INTEGER, data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT]) + if matches is None: + errors[CONFIG_TARGET_DANGEROUS_SETTINGS] = "invalid_integer" + else: + minimum_required_minutes_in_slot = int(data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT]) + data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] = minimum_required_minutes_in_slot + else: + errors[CONFIG_TARGET_DANGEROUS_SETTINGS] = "invalid_integer" + else: + minimum_required_minutes_in_slot = data[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] + + if minimum_required_minutes_in_slot is not None and (minimum_required_minutes_in_slot < 1 or minimum_required_minutes_in_slot > 30): + errors[CONFIG_TARGET_DANGEROUS_SETTINGS] = "invalid_minimum_required_minutes_in_slot" + return errors \ No newline at end of file diff --git a/custom_components/target_timeframes/config_flow.py b/custom_components/target_timeframes/config_flow.py index 781e59c..5b82a0d 100644 --- a/custom_components/target_timeframes/config_flow.py +++ b/custom_components/target_timeframes/config_flow.py @@ -7,7 +7,6 @@ from .config.rolling_target_timeframe import merge_rolling_target_timeframe_config, validate_rolling_target_timeframe_config from .const import ( - CONFIG_DATA_SOURCE_ID, CONFIG_DATA_UNIQUE_ID, CONFIG_KIND, CONFIG_KIND_ROLLING_TARGET_RATE, diff --git a/custom_components/target_timeframes/const.py b/custom_components/target_timeframes/const.py index a1175e4..1152329 100644 --- a/custom_components/target_timeframes/const.py +++ b/custom_components/target_timeframes/const.py @@ -1,6 +1,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers import selector +from homeassistant.data_entry_flow import section DOMAIN = "target_timeframes" INTEGRATION_VERSION = "1.2.1" @@ -37,6 +38,10 @@ CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST = "all_target_times_in_past" CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST = "all_target_times_in_future_or_past" CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS = "always" +CONFIG_TARGET_DANGEROUS_SETTINGS = "dangerous_settings" +CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA = "calculate_with_incomplete_data" +CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT = "minimum_required_minutes_in_slot" +CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT = 29 CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD = "look_ahead_hours" @@ -54,7 +59,8 @@ CONFIG_TARGET_MAX_VALUE, CONFIG_TARGET_WEIGHTING, CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD, - CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE + CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE, + CONFIG_TARGET_DANGEROUS_SETTINGS ] REGEX_HOURS = "^[0-9]+(\\.[0-9]+)*$" @@ -63,6 +69,7 @@ REGEX_OFFSET_PARTS = "^(-)?([0-1]?[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$" REGEX_DATE = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" REGEX_VALUE = "^(-)?[0-9]+(\\.[0-9]+)*$" +REGEX_INTEGER = "^(-)?[0-9]+$" REGEX_WEIGHTING_NUMBERS = "([0-9]+\\.?[0-9]*(,[0-9]+\\.?[0-9]*+)*)" REGEX_WEIGHTING_START = "(\\*(,[0-9]+\\.?[0-9]*+)+)" @@ -112,9 +119,18 @@ vol.Optional(CONFIG_TARGET_ROLLING_TARGET, default=False): bool, vol.Optional(CONFIG_TARGET_LATEST_VALUES, default=False): bool, vol.Optional(CONFIG_TARGET_FIND_HIGHEST_VALUES, default=False): bool, - vol.Optional(CONFIG_TARGET_MIN_VALUE): vol.Coerce(float), - vol.Optional(CONFIG_TARGET_MAX_VALUE): vol.Coerce(float), + vol.Optional(CONFIG_TARGET_MIN_VALUE): float, + vol.Optional(CONFIG_TARGET_MAX_VALUE): float, vol.Optional(CONFIG_TARGET_WEIGHTING): str, + vol.Required(CONFIG_TARGET_DANGEROUS_SETTINGS): section( + vol.Schema( + { + vol.Required(CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA, default=False): bool, + vol.Required(CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT, default=CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT): int, + } + ), + {"collapsed": True}, + ), }) DATA_SCHEMA_ROLLING_TARGET_TIME_PERIOD = vol.Schema({ @@ -153,9 +169,18 @@ ), vol.Optional(CONFIG_TARGET_LATEST_VALUES): bool, vol.Optional(CONFIG_TARGET_FIND_HIGHEST_VALUES): bool, - vol.Optional(CONFIG_TARGET_MIN_VALUE): vol.Coerce(float), - vol.Optional(CONFIG_TARGET_MAX_VALUE): vol.Coerce(float), + vol.Optional(CONFIG_TARGET_MIN_VALUE): float, + vol.Optional(CONFIG_TARGET_MAX_VALUE): float, vol.Optional(CONFIG_TARGET_WEIGHTING): str, + vol.Required(CONFIG_TARGET_DANGEROUS_SETTINGS): section( + vol.Schema( + { + vol.Required(CONFIG_TARGET_CALCULATE_WITH_INCOMPLETE_DATA, default=False): bool, + vol.Required(CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT, default=CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT): int, + } + ), + {"collapsed": True}, + ), }) EVENT_DATA_SOURCE = "target_time_period_data_source_updated" \ No newline at end of file diff --git a/custom_components/target_timeframes/entities/__init__.py b/custom_components/target_timeframes/entities/__init__.py index fd85c4e..7e223ea 100644 --- a/custom_components/target_timeframes/entities/__init__.py +++ b/custom_components/target_timeframes/entities/__init__.py @@ -6,7 +6,7 @@ from homeassistant.util.dt import (as_utc, parse_datetime) -from ..const import 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_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 _LOGGER = logging.getLogger(__name__) @@ -43,7 +43,7 @@ def is_target_timeframe_complete_in_period(current_date: datetime, start_time: d target_timeframes[-1]["end"] <= current_date ) -def get_start_and_end_times(current_date: datetime, target_start_time: str, target_end_time: str, start_time_not_in_past = True, context: str = None): +def get_start_and_end_times(current_date: datetime, target_start_time: str, target_end_time: str, minimum_slot_minutes = None, context: str = None): if (target_start_time is not None): target_start = parse_datetime(current_date.strftime(f"%Y-%m-%dT{target_start_time}:00%z")) else: @@ -64,10 +64,15 @@ def get_start_and_end_times(current_date: datetime, target_start_time: str, targ else: target_end = target_end + timedelta(days=1) - # If our start date has passed, reset it to current_date to avoid picking a slot in the past - if (start_time_not_in_past == True and target_start < current_date and current_date < target_end): - _LOGGER.debug(f'{context} - Rolling target and {target_start} is in the past. Setting start to {current_date}') - target_start = current_date + if (minimum_slot_minutes is not None and target_start < current_date and current_date < target_end): + current_date_start = current_date.replace(minute=30 if current_date.minute >= 30 else 0, second=0, microsecond=0) + minutes_remaining_in_current_slot = 30 - ((current_date.replace(second=0, microsecond=0) - current_date_start).total_seconds() / 60) + if (minutes_remaining_in_current_slot >= minimum_slot_minutes): + _LOGGER.debug(f'{context} - Current slot is sufficient for minimum slot minutes, so using current date start: {current_date_start}') + target_start = current_date_start + else: + target_start = current_date_start + timedelta(minutes=30) + _LOGGER.debug(f'{context} - Current slot is not sufficient for minimum slot minutes, so using next slot start: {target_start}') # 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): @@ -76,7 +81,7 @@ def get_start_and_end_times(current_date: datetime, target_start_time: str, targ return (target_start, target_end) -def get_fixed_applicable_time_periods(target_start: datetime, target_end: datetime, time_period_values: list, context: str = None): +def get_fixed_applicable_time_periods(target_start: datetime, target_end: datetime, time_period_values: list, calculate_with_incomplete_data = False, context: str = None): _LOGGER.debug(f'{context} - Finding rates between {target_start} and {target_end}') # Retrieve the rates that are applicable for our target rate @@ -92,28 +97,35 @@ def get_fixed_applicable_time_periods(target_start: datetime, target_end: dateti date_diff = target_end - target_start hours = (date_diff.days * 24) + (date_diff.seconds // 3600) periods = hours * 2 - if len(applicable_rates) < periods: + if len(applicable_rates) < periods and calculate_with_incomplete_data == False: _LOGGER.debug(f'{context} - Incorrect number of periods discovered. Require {periods}, but only have {len(applicable_rates)}') return None return applicable_rates -def get_rolling_applicable_time_periods(current_date: datetime, time_period_values: list, target_hours: float, context: str = None): +def get_rolling_applicable_time_periods(current_date: datetime, + time_period_values: list, + target_hours: float, + minimum_slot_minutes: int = CONFIG_TARGET_DEFAULT_MINIMUM_REQUIRED_MINUTES_IN_SLOT, + calculate_with_incomplete_data = False, + context: str = None): # Retrieve the rates that are applicable for our target rate applicable_time_periods = [] periods = target_hours * 2 if time_period_values is not None: - for rate in time_period_values: - if rate["end"] >= current_date: - new_rate = dict(rate) + for time_period in time_period_values: + + minutes_remaining_in_current_slot = 30 - ((current_date.replace(second=0, microsecond=0) - time_period["start"]).total_seconds() / 60) + if minutes_remaining_in_current_slot >= minimum_slot_minutes and time_period["end"] >= current_date: + new_rate = dict(time_period) applicable_time_periods.append(new_rate) if len(applicable_time_periods) >= periods: break # Make sure that we have enough rates that meet our target period - if len(applicable_time_periods) < periods: + if len(applicable_time_periods) < periods and calculate_with_incomplete_data == False: _LOGGER.debug(f'{context} - Incorrect number of periods discovered. Require {periods}, but only have {len(applicable_time_periods)}') return None diff --git a/custom_components/target_timeframes/entities/rolling_target_timeframe.py b/custom_components/target_timeframes/entities/rolling_target_timeframe.py index d258dca..732d88f 100644 --- a/custom_components/target_timeframes/entities/rolling_target_timeframe.py +++ b/custom_components/target_timeframes/entities/rolling_target_timeframe.py @@ -20,6 +20,10 @@ from ..const import ( CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD, + 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, CONFIG_TARGET_HOURS_MODE, CONFIG_TARGET_MAX_VALUE, @@ -69,16 +73,7 @@ def __init__(self, hass: HomeAssistant, data_source_id: str, config_entry, confi self._last_evaluated = None self._data_source_id = data_source_id 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 - - 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 + self.update_default_attributes() self._data_source_data = initial_data if initial_data is not None else [] self._target_timeframes = [] @@ -150,10 +145,20 @@ async def async_update(self): if CONFIG_TARGET_MAX_VALUE in self._config: max_value = self._config[CONFIG_TARGET_MAX_VALUE] + 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] + + minimum_slot_minutes = 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_slot_minutes = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] + applicable_time_periods = get_rolling_applicable_time_periods( current_local_date, self._data_source_data, self._config[CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD], + minimum_slot_minutes, + calculate_with_incomplete_data, self._config[CONFIG_TARGET_NAME] ) @@ -191,7 +196,7 @@ async def async_update(self): self._attributes["target_times_last_evaluated"] = current_date _LOGGER.debug(f"{self._config[CONFIG_TARGET_NAME]} - calculated rates: {self._target_timeframes}") - self._attributes["time_periods_incomplete"] = applicable_time_periods is None + self._attributes["time_periods_incomplete"] = applicable_time_periods is None or len(applicable_time_periods) < (target_hours * 2) active_result = get_target_time_period_info(current_date, self._target_timeframes, offset) @@ -242,6 +247,7 @@ async def async_added_to_hass(self): if compare_config(self._config, self._attributes) == False: self._state = False self._attributes = self._config.copy() + self.update_default_attributes() self._target_timeframes = None _LOGGER.debug(f'{self._config[CONFIG_TARGET_NAME]} - Restored state: {self._state}') @@ -300,6 +306,7 @@ async def async_update_rolling_target_timeframe_config(self, target_hours=None, self._config = config self._attributes = self._config.copy() + self.update_default_attributes() self._target_timeframes = [] await self.async_update() self.async_write_ha_state() @@ -313,4 +320,26 @@ async def async_update_rolling_target_timeframe_config(self, target_hours=None, self._config_entry, self._config_subentry, data = new_config_data - ) \ No newline at end of file + ) + + 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 + + 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 + + if CONFIG_TARGET_DANGEROUS_SETTINGS in self._attributes: + del self._attributes[CONFIG_TARGET_DANGEROUS_SETTINGS] \ No newline at end of file diff --git a/custom_components/target_timeframes/entities/target_timeframe.py b/custom_components/target_timeframes/entities/target_timeframe.py index c9c4a28..9d7c11a 100644 --- a/custom_components/target_timeframes/entities/target_timeframe.py +++ b/custom_components/target_timeframes/entities/target_timeframe.py @@ -20,6 +20,10 @@ from homeassistant.helpers import translation 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, CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, CONFIG_TARGET_HOURS_MODE, @@ -73,17 +77,8 @@ def __init__(self, hass: HomeAssistant, data_source_id: str, config_entry, confi self._attributes = self._config.copy() self._last_evaluated = None self._data_source_id = data_source_id - 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 - 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 + self.update_default_attributes() self._data_source_data = initial_data if initial_data is not None else [] self._target_timeframes = [] @@ -167,16 +162,31 @@ async def async_update(self): if CONFIG_TARGET_MAX_VALUE in self._config: max_rate = self._config[CONFIG_TARGET_MAX_VALUE] - target_start, target_end = get_start_and_end_times(current_local_date, start_time, end_time, True, self._config[CONFIG_TARGET_NAME]) + 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] + + minimum_slot_minutes = 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_slot_minutes = self._config[CONFIG_TARGET_DANGEROUS_SETTINGS][CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT] + + target_start, target_end = get_start_and_end_times( + current_local_date, + start_time, + end_time, + minimum_slot_minutes, + self._config[CONFIG_TARGET_NAME] + ) applicable_time_periods = get_fixed_applicable_time_periods( target_start, target_end, self._data_source_data, + calculate_with_incomplete_data, self._config[CONFIG_TARGET_NAME] ) # Make sure we haven't already completed for the current target timeframe - applicable_target_start, applicable_target_end = get_start_and_end_times(current_local_date, start_time, end_time, False, self._config[CONFIG_TARGET_NAME]) + applicable_target_start, applicable_target_end = get_start_and_end_times(current_local_date, start_time, end_time, None, self._config[CONFIG_TARGET_NAME]) is_target_timeframe_complete = is_rolling_target == False and is_target_timeframe_complete_in_period( current_local_date, applicable_target_start, @@ -220,7 +230,7 @@ async def async_update(self): self._attributes["target_times_last_evaluated"] = current_date _LOGGER.debug(f"{self._config[CONFIG_TARGET_NAME]} - calculated rates: {self._target_timeframes}") - self._attributes["time_periods_incomplete"] = applicable_time_periods is None + self._attributes["time_periods_incomplete"] = applicable_time_periods is None or len(applicable_time_periods) < (target_hours * 2) active_result = get_target_time_period_info(current_date, self._target_timeframes, offset) @@ -271,6 +281,7 @@ async def async_added_to_hass(self): if compare_config(self._config, self._attributes) == False: self._state = False self._attributes = self._config.copy() + self.update_default_attributes() self._target_timeframes = None _LOGGER.debug(f'{self._config[CONFIG_TARGET_NAME]} - Restored state: {self._state}') @@ -335,6 +346,7 @@ async def async_update_target_timeframe_config(self, target_start_time=None, tar self._config = config self._attributes = self._config.copy() + self.update_default_attributes() self._target_timeframes = [] await self.async_update() self.async_write_ha_state() @@ -348,4 +360,31 @@ async def async_update_target_timeframe_config(self, target_start_time=None, tar self._config_entry, self._config_subentry, data = new_config_data - ) \ No newline at end of file + ) + + 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 + + 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] \ No newline at end of file diff --git a/custom_components/target_timeframes/translations/en.json b/custom_components/target_timeframes/translations/en.json index 0e4ab75..e41edeb 100644 --- a/custom_components/target_timeframes/translations/en.json +++ b/custom_components/target_timeframes/translations/en.json @@ -24,6 +24,9 @@ "source_id": "This must be unique across all sources. This is used in all related entity names. Changing will create new entities." } } + }, + "abort": { + "reconfigure_successful": "Reconfiguration successful" } }, "config_subentries": { @@ -57,6 +60,19 @@ "hours": "This has to be a multiple of 0.5.", "minimum_value": "This is in decimal format (e.g. 0.12)", "maximum_value": "This is in decimal format (e.g. 0.12)" + }, + "sections": { + "dangerous_settings": { + "name": "Dangerous settings", + "description": "These settings can have undesired consequences if changed. Change at your own risk.", + "data": { + "calculate_with_incomplete_data": "Calculate with incomplete data", + "minimum_required_minutes_in_slot": "Minimum required minutes in slot" + }, + "data_description": { + "minimum_required_minutes_in_slot": "The minimum number of minutes that must be present in a slot for it to be considered. Changing this can cause chosen slots to be partially in the past and therefore turn on for less than the desired time." + } + } } }, "reconfigure": { @@ -83,9 +99,25 @@ "hours": "This has to be a multiple of 0.5.", "minimum_value": "This is in decimal format (e.g. 0.12)", "maximum_value": "This is in decimal format (e.g. 0.12)" + }, + "sections": { + "dangerous_settings": { + "name": "Dangerous settings", + "description": "These settings can have undesired consequences if changed. Change at your own risk.", + "data": { + "calculate_with_incomplete_data": "Calculate with incomplete data", + "minimum_required_minutes_in_slot": "Minimum required minutes in slot" + }, + "data_description": { + "minimum_required_minutes_in_slot": "The minimum number of minutes that must be present in a slot for it to be considered. Changing this can cause chosen slots to be partially in the past and therefore turn on for less than the desired time." + } + } } } }, + "abort": { + "reconfigure_successful": "Reconfiguration successful" + }, "error": { "value_greater_than_zero": "Value must be greater or equal to 1", "invalid_target_hours": "Hours must be in half hour increments (e.g. 0.5 = 30 minutes; 1 = 60 minutes).", @@ -98,7 +130,10 @@ "invalid_weighting_slots": "The number of weighting blocks does not equal the specified number of hours.", "weighting_not_supported_for_type": "Weighting is only supported for continuous target values", "weighting_not_supported_for_hour_mode": "Weighting is not supported for this hour mode", - "minimum_or_maximum_value_not_specified": "Either minimum and/or maximum value must be specified for minimum hours mode" + "minimum_or_maximum_value_not_specified": "Either minimum and/or maximum value must be specified for minimum hours mode", + "minimum_value_not_less_than_maximum_value": "Minimum value must be less or equal to the maximum value if both are specified", + "invalid_integer": "Value must be a number with no decimal places", + "invalid_minimum_required_minutes_in_slot": "Value must be between 1 and 30" } }, "rolling_target_time_period": { @@ -122,6 +157,15 @@ "maximum_value": "The optional maximum value for target hours", "weighting": "The optional weighting to apply to the discovered time period values", "target_times_evaluation_mode": "When should target times be selected" + }, + "sections": { + "dangerous_settings": { + "name": "Dangerous settings", + "description": "These settings can have undesired consequences if changed. Change at your own risk.", + "data": { + "calculate_with_incomplete_data": "Calculate with incomplete data" + } + } } }, "reconfigure": { @@ -139,9 +183,21 @@ "maximum_value": "The optional maximum value for target hours", "weighting": "The optional weighting to apply to the discovered time period values", "target_times_evaluation_mode": "When should target times be selected" + }, + "sections": { + "dangerous_settings": { + "name": "Dangerous settings", + "description": "These settings can have undesired consequences if changed. Change at your own risk.", + "data": { + "calculate_with_incomplete_data": "Calculate with incomplete data" + } + } } } }, + "abort": { + "reconfigure_successful": "Reconfiguration successful" + }, "error": { "value_greater_than_zero": "Value must be greater or equal to 1", "invalid_target_hours": "Hours must be in half hour increments (e.g. 0.5 = 30 minutes; 1 = 60 minutes).", @@ -154,7 +210,10 @@ "invalid_weighting_slots": "The number of weighting blocks does not equal the specified number of hours.", "weighting_not_supported_for_type": "Weighting is only supported for continuous target values", "weighting_not_supported_for_hour_mode": "Weighting is not supported for this hour mode", - "minimum_or_maximum_value_not_specified": "Either minimum and/or maximum value must be specified for minimum hours mode" + "minimum_or_maximum_value_not_specified": "Either minimum and/or maximum value must be specified for minimum hours mode", + "minimum_value_not_less_than_maximum_value": "Minimum value must be less or equal to the maximum value if both are specified", + "invalid_integer": "Value must be a number with no decimal places", + "invalid_minimum_required_minutes_in_slot": "Value must be between 1 and 30" } } }, diff --git a/tests/unit/config/__init__.py b/tests/unit/config/__init__.py index 71a786a..709d7ff 100644 --- a/tests/unit/config/__init__.py +++ b/tests/unit/config/__init__.py @@ -1,5 +1,18 @@ -def assert_errors_not_present(errors, config_keys: list, key_to_ignore: str = None): - for key in config_keys: +import voluptuous as vol + +def get_schema_keys(schema): + keys = [] + for k in schema.keys(): + if isinstance(k, vol.Marker): # Required/Optional wrapper + keys.append(k.schema) # actual key name + else: + keys.append(k) + return keys + +def assert_errors_not_present(errors, default_keys: list[str], key_to_ignore: str = None): + assert len(default_keys) > 0 + + for key in default_keys: if key_to_ignore is not None and key == key_to_ignore: continue diff --git a/tests/unit/config/test_validate_rolling_target_timeframe_config.py b/tests/unit/config/test_validate_rolling_target_timeframe_config.py new file mode 100644 index 0000000..15c681a --- /dev/null +++ b/tests/unit/config/test_validate_rolling_target_timeframe_config.py @@ -0,0 +1,456 @@ +import pytest + +from homeassistant.util.dt import (as_utc, parse_datetime) +from custom_components.target_timeframes.config.rolling_target_timeframe import validate_rolling_target_timeframe_config +from custom_components.target_timeframes.const import CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD, CONFIG_TARGET_DANGEROUS_SETTINGS, CONFIG_TARGET_HOURS, CONFIG_TARGET_HOURS_MODE, CONFIG_TARGET_HOURS_MODE_EXACT, CONFIG_TARGET_HOURS_MODE_MAXIMUM, CONFIG_TARGET_HOURS_MODE_MINIMUM, CONFIG_TARGET_MAX_VALUE, CONFIG_TARGET_MIN_VALUE, CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT, CONFIG_TARGET_NAME, CONFIG_TARGET_OFFSET, CONFIG_TARGET_TYPE, CONFIG_TARGET_TYPE_CONTINUOUS, CONFIG_TARGET_TYPE_INTERMITTENT, CONFIG_TARGET_WEIGHTING, DATA_SCHEMA_ROLLING_TARGET_TIME_PERIOD +from ..config import assert_errors_not_present, get_schema_keys + +now = as_utc(parse_datetime("2023-08-20T10:00:00Z")) + +default_keys = get_schema_keys(DATA_SCHEMA_ROLLING_TARGET_TIME_PERIOD.schema) + +@pytest.mark.asyncio +async def test_when_config_is_valid_no_errors_returned(): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_OFFSET: "-00:30:00", + CONFIG_TARGET_MIN_VALUE: "0", + CONFIG_TARGET_MAX_VALUE: "10", + CONFIG_TARGET_WEIGHTING: "2,*,2", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert_errors_not_present(errors, default_keys) + +@pytest.mark.asyncio +async def test_when_optional_config_is_valid_no_errors_returned(): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_OFFSET: None, + CONFIG_TARGET_MIN_VALUE: None, + CONFIG_TARGET_MAX_VALUE: None, + CONFIG_TARGET_WEIGHTING: None, + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert_errors_not_present(errors, default_keys) + +@pytest.mark.asyncio +@pytest.mark.parametrize("name",[ + (""), + ("Test"), + ("test@"), +]) +async def test_when_config_has_invalid_name_then_errors_returned(name): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: name, + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_OFFSET: "-00:00:00", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert CONFIG_TARGET_NAME in errors + assert errors[CONFIG_TARGET_NAME] == "invalid_target_name" + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_NAME) + +@pytest.mark.asyncio +async def test_when_config_has_valid_hours_then_no_errors_returned(): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "0", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_OFFSET: "-00:00:00", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert_errors_not_present(errors, default_keys) + +@pytest.mark.asyncio +@pytest.mark.parametrize("hours",[ + (""), + ("-1.0"), + ("s"), + ("1.01"), + ("1.49"), + ("1.51"), + ("1.99"), +]) +async def test_when_config_has_invalid_hours_then_errors_returned(hours): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: hours, + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_OFFSET: "-00:00:00", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert CONFIG_TARGET_HOURS in errors + assert errors[CONFIG_TARGET_HOURS] == "invalid_target_hours" + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_HOURS) + +@pytest.mark.asyncio +@pytest.mark.parametrize("look_ahead",[ + (""), + ("s"), + ("-0"), + ("-0.01"), +]) +async def test_when_config_has_invalid_look_ahead_hours_then_errors_returned(look_ahead): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: look_ahead, + CONFIG_TARGET_OFFSET: "-00:00:00", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD in errors + assert errors[CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD] == "invalid_target_hours" + assert_errors_not_present(errors, default_keys, CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD) + +@pytest.mark.asyncio +@pytest.mark.parametrize("offset",[ + (""), + ("s"), + ("00"), + ("-00"), + ("00:00"), + ("-00:00"), + ("24:00:00"), + ("-24:00:00"), + ("00:60:00"), + ("-00:60:00"), + ("00:00:60"), + ("-00:00:60"), +]) +async def test_when_config_has_invalid_offset_then_errors_returned(offset): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_OFFSET: offset, + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT, + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert CONFIG_TARGET_OFFSET in errors + assert errors[CONFIG_TARGET_OFFSET] == "invalid_offset" + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_OFFSET) + +@pytest.mark.asyncio +async def test_when_hours_exceed_selected_look_ahead_hours_then_errors_returned(): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "1.49", + CONFIG_TARGET_OFFSET: "-00:00:00", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT, + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD in errors + assert errors[CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD] == "invalid_target_hours" + assert_errors_not_present(errors, default_keys, CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD) + +@pytest.mark.asyncio +@pytest.mark.parametrize("weighting,expected_error",[ + ("*", "invalid_weighting"), + ("*,*", "invalid_weighting"), + ("1,*,1,*", "invalid_weighting"), + ("a,*", "invalid_weighting"), + ("1,2", "invalid_weighting_slots"), + ("1,2,3,4", "invalid_weighting_slots"), +]) +async def test_when_weighting_is_invalid_then_weighting_error_returned(weighting, expected_error): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_WEIGHTING: weighting, + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT, + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert CONFIG_TARGET_WEIGHTING in errors + assert errors[CONFIG_TARGET_WEIGHTING] == expected_error + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_WEIGHTING) + +@pytest.mark.asyncio +@pytest.mark.parametrize("type",[ + (CONFIG_TARGET_TYPE_INTERMITTENT), +]) +async def test_when_weighting_set_and_type_invalid_then_weighting_error_returned(type): + # Arrange + data = { + CONFIG_TARGET_TYPE: type, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_WEIGHTING: "1,2,3", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT, + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert CONFIG_TARGET_WEIGHTING in errors + assert errors[CONFIG_TARGET_WEIGHTING] == "weighting_not_supported_for_type" + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_WEIGHTING) + +@pytest.mark.asyncio +@pytest.mark.parametrize("min_value,max_value",[ + (None,"1.5"), + (None,1.5), + (None,"-1.5"), + (None,-1.5), + ("1.5",None), + (1.5,None), + ("-1.5",None), + (-1.5,None), + ("1.5","2.0"), + (1.5,2.0), + ("-2.0","-1.5"), + (-2.0,-1.5), +]) +async def test_when_hour_mode_is_minimum_and_minimum_or_maximum_value_is_specified_then_no_error_returned(min_value: float, max_value: float): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_OFFSET: "-00:30:00", + CONFIG_TARGET_MIN_VALUE: min_value, + CONFIG_TARGET_MAX_VALUE: max_value, + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_MINIMUM + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert_errors_not_present(errors, default_keys) + +@pytest.mark.asyncio +@pytest.mark.parametrize("min_value,max_value",[ + ("1.5","1.49"), + (1.5,1.49), + ("-1.49","-1.5"), + (-1.49,-1.5), +]) +async def test_when_minimum_value_greater_to_maximum_value_is_specified_then_error_returned(min_value: float, max_value: float): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_OFFSET: "-00:30:00", + CONFIG_TARGET_MIN_VALUE: min_value, + CONFIG_TARGET_MAX_VALUE: max_value, + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_MINIMUM + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert CONFIG_TARGET_MIN_VALUE in errors + assert errors[CONFIG_TARGET_MIN_VALUE] == "minimum_value_not_less_than_maximum_value" + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_MIN_VALUE) + +@pytest.mark.asyncio +@pytest.mark.parametrize("hour_mode",[ + (CONFIG_TARGET_HOURS_MODE_MINIMUM), + (CONFIG_TARGET_HOURS_MODE_MAXIMUM), +]) +async def test_when_hour_mode_is_not_exact_and_weighting_specified_then_error_returned(hour_mode: str): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_OFFSET: "-00:30:00", + CONFIG_TARGET_WEIGHTING: "2,*,2", + CONFIG_TARGET_MIN_VALUE: "0.18", + CONFIG_TARGET_HOURS_MODE: hour_mode + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert CONFIG_TARGET_WEIGHTING in errors + assert errors[CONFIG_TARGET_WEIGHTING] == "weighting_not_supported_for_hour_mode" + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_WEIGHTING) + +@pytest.mark.asyncio +async def test_when_hour_mode_is_minimum_and_minimum_and_maximum_value_is_not_specified_then_error_returned(): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_OFFSET: "-00:30:00", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_MINIMUM + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert CONFIG_TARGET_HOURS_MODE in errors + assert errors[CONFIG_TARGET_HOURS_MODE] == "minimum_or_maximum_value_not_specified" + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_HOURS_MODE) + +@pytest.mark.asyncio +@pytest.mark.parametrize("minimum_required_minutes",[ + ("30"), + (30), + ("1"), + (1), +]) +async def test_when_minimum_required_minutes_set_to_valid_integer_then_no_error_returned(minimum_required_minutes): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_OFFSET: "-00:30:00", + CONFIG_TARGET_MIN_VALUE: "0", + CONFIG_TARGET_MAX_VALUE: "10", + CONFIG_TARGET_WEIGHTING: "2,*,2", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT, + CONFIG_TARGET_DANGEROUS_SETTINGS: { + CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: minimum_required_minutes + } + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert_errors_not_present(errors, default_keys) + +@pytest.mark.asyncio +@pytest.mark.parametrize("minimum_required_minutes",[ + ("30.1"), + (30.1), + ("a"), +]) +async def test_when_minimum_required_minutes_set_to_invalid_integer_then_error_returned(minimum_required_minutes): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_OFFSET: "-00:30:00", + CONFIG_TARGET_MIN_VALUE: "0", + CONFIG_TARGET_MAX_VALUE: "10", + CONFIG_TARGET_WEIGHTING: "2,*,2", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT, + CONFIG_TARGET_DANGEROUS_SETTINGS: { + CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: minimum_required_minutes + } + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert CONFIG_TARGET_DANGEROUS_SETTINGS in errors + assert errors[CONFIG_TARGET_DANGEROUS_SETTINGS] == "invalid_integer" + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_DANGEROUS_SETTINGS) + +@pytest.mark.asyncio +@pytest.mark.parametrize("minimum_required_minutes",[ + ("0"), + (0), + ("31"), + (31), +]) +async def test_when_minimum_required_minutes_set_to_invalid_value_then_error_returned(minimum_required_minutes): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD: "2", + CONFIG_TARGET_OFFSET: "-00:30:00", + CONFIG_TARGET_MIN_VALUE: "0", + CONFIG_TARGET_MAX_VALUE: "10", + CONFIG_TARGET_WEIGHTING: "2,*,2", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT, + CONFIG_TARGET_DANGEROUS_SETTINGS: { + CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: minimum_required_minutes + } + } + + # Act + errors = validate_rolling_target_timeframe_config(data) + + # Assert + assert CONFIG_TARGET_DANGEROUS_SETTINGS in errors + assert errors[CONFIG_TARGET_DANGEROUS_SETTINGS] == "invalid_minimum_required_minutes_in_slot" + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_DANGEROUS_SETTINGS) \ No newline at end of file diff --git a/tests/unit/config/test_validate_target_timeframe_config.py b/tests/unit/config/test_validate_target_timeframe_config.py index 2ad2f03..2426430 100644 --- a/tests/unit/config/test_validate_target_timeframe_config.py +++ b/tests/unit/config/test_validate_target_timeframe_config.py @@ -2,10 +2,13 @@ from homeassistant.util.dt import (as_utc, parse_datetime) from custom_components.target_timeframes.config.target_timeframe import validate_target_timeframe_config -from custom_components.target_timeframes.const import CONFIG_TARGET_END_TIME, CONFIG_TARGET_HOURS, CONFIG_TARGET_HOURS_MODE, CONFIG_TARGET_HOURS_MODE_EXACT, CONFIG_TARGET_HOURS_MODE_MAXIMUM, CONFIG_TARGET_HOURS_MODE_MINIMUM, CONFIG_TARGET_MAX_VALUE, CONFIG_TARGET_MIN_VALUE, CONFIG_TARGET_NAME, CONFIG_TARGET_OFFSET, CONFIG_TARGET_START_TIME, CONFIG_TARGET_TYPE, CONFIG_TARGET_TYPE_CONTINUOUS, CONFIG_TARGET_TYPE_INTERMITTENT, CONFIG_TARGET_WEIGHTING +from custom_components.target_timeframes.const import CONFIG_TARGET_DANGEROUS_SETTINGS, CONFIG_TARGET_END_TIME, CONFIG_TARGET_HOURS, CONFIG_TARGET_HOURS_MODE, CONFIG_TARGET_HOURS_MODE_EXACT, CONFIG_TARGET_HOURS_MODE_MAXIMUM, CONFIG_TARGET_HOURS_MODE_MINIMUM, CONFIG_TARGET_MAX_VALUE, CONFIG_TARGET_MIN_VALUE, CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT, CONFIG_TARGET_NAME, CONFIG_TARGET_OFFSET, CONFIG_TARGET_START_TIME, CONFIG_TARGET_TYPE, CONFIG_TARGET_TYPE_CONTINUOUS, CONFIG_TARGET_TYPE_INTERMITTENT, CONFIG_TARGET_WEIGHTING, DATA_SCHEMA_ROLLING_TARGET_TIME_PERIOD, DATA_SCHEMA_TARGET_TIME_PERIOD +from ..config import assert_errors_not_present, get_schema_keys now = as_utc(parse_datetime("2023-08-20T10:00:00Z")) +default_keys = get_schema_keys(DATA_SCHEMA_TARGET_TIME_PERIOD.schema) + @pytest.mark.asyncio async def test_when_config_is_valid_no_errors_returned(): # Arrange @@ -26,15 +29,7 @@ async def test_when_config_is_valid_no_errors_returned(): errors = validate_target_timeframe_config(data) # Assert - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_WEIGHTING not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys) @pytest.mark.asyncio async def test_when_optional_config_is_valid_no_errors_returned(): @@ -56,15 +51,7 @@ async def test_when_optional_config_is_valid_no_errors_returned(): errors = validate_target_timeframe_config(data) # Assert - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_WEIGHTING not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys) @pytest.mark.asyncio @pytest.mark.parametrize("name",[ @@ -90,15 +77,7 @@ async def test_when_config_has_invalid_name_then_errors_returned(name): # Assert assert CONFIG_TARGET_NAME in errors assert errors[CONFIG_TARGET_NAME] == "invalid_target_name" - - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_WEIGHTING not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_NAME) @pytest.mark.asyncio async def test_when_config_has_valid_hours_then_no_errors_returned(): @@ -117,15 +96,7 @@ async def test_when_config_has_valid_hours_then_no_errors_returned(): errors = validate_target_timeframe_config(data) # Assert - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_WEIGHTING not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys) @pytest.mark.asyncio @pytest.mark.parametrize("hours",[ @@ -155,15 +126,7 @@ async def test_when_config_has_invalid_hours_then_errors_returned(hours): # Assert assert CONFIG_TARGET_HOURS in errors assert errors[CONFIG_TARGET_HOURS] == "invalid_target_hours" - - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_WEIGHTING not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_HOURS) @pytest.mark.asyncio @pytest.mark.parametrize("start_time",[ @@ -193,15 +156,7 @@ async def test_when_config_has_invalid_start_time_then_errors_returned(start_tim # Assert assert CONFIG_TARGET_START_TIME in errors assert errors[CONFIG_TARGET_START_TIME] == "invalid_target_time" - - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_WEIGHTING not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_START_TIME) @pytest.mark.asyncio @pytest.mark.parametrize("end_time",[ @@ -231,15 +186,7 @@ async def test_when_config_has_invalid_end_time_then_errors_returned(end_time): # Assert assert CONFIG_TARGET_END_TIME in errors assert errors[CONFIG_TARGET_END_TIME] == "invalid_target_time" - - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_WEIGHTING not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_END_TIME) @pytest.mark.asyncio @pytest.mark.parametrize("offset",[ @@ -274,15 +221,7 @@ async def test_when_config_has_invalid_offset_then_errors_returned(offset): # Assert assert CONFIG_TARGET_OFFSET in errors assert errors[CONFIG_TARGET_OFFSET] == "invalid_offset" - - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_WEIGHTING not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_OFFSET) @pytest.mark.asyncio @pytest.mark.parametrize("start_time,end_time",[ @@ -307,15 +246,7 @@ async def test_when_hours_exceed_selected_time_frame_then_errors_returned(start_ # Assert assert CONFIG_TARGET_HOURS in errors assert errors[CONFIG_TARGET_HOURS] == "invalid_hours_time_frame" - - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_WEIGHTING not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_HOURS) @pytest.mark.asyncio @pytest.mark.parametrize("start_time,end_time,offset",[ @@ -344,15 +275,7 @@ async def test_when_config_is_valid_and_not_agile_then_no_errors_returned(start_ errors = validate_target_timeframe_config(data) # Assert - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_WEIGHTING not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys) @pytest.mark.asyncio @pytest.mark.parametrize("start_time,end_time,offset",[ @@ -389,15 +312,7 @@ async def test_when_config_is_valid_and_agile_then_no_errors_returned(start_time errors = validate_target_timeframe_config(data) # Assert - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_WEIGHTING not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_NAME) @pytest.mark.asyncio @pytest.mark.parametrize("weighting,expected_error",[ @@ -424,15 +339,7 @@ async def test_when_weighting_is_invalid_then_weighting_error_returned(weighting # Assert assert CONFIG_TARGET_WEIGHTING in errors assert errors[CONFIG_TARGET_WEIGHTING] == expected_error - - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_WEIGHTING) @pytest.mark.asyncio @pytest.mark.parametrize("type",[ @@ -454,21 +361,22 @@ async def test_when_weighting_set_and_type_invalid_then_weighting_error_returned # Assert assert CONFIG_TARGET_WEIGHTING in errors assert errors[CONFIG_TARGET_WEIGHTING] == "weighting_not_supported_for_type" - - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_WEIGHTING) @pytest.mark.asyncio @pytest.mark.parametrize("min_value,max_value",[ (None,"1.5"), + (None,1.5), + (None,"-1.5"), + (None,-1.5), ("1.5",None), + (1.5,None), + ("-1.5",None), + (-1.5,None), ("1.5","2.0"), + (1.5,2.0), + ("-2.0","-1.5"), + (-2.0,-1.5), ]) async def test_when_hour_mode_is_minimum_and_minimum_or_maximum_value_is_specified_then_no_error_returned(min_value: float, max_value: float): # Arrange @@ -488,15 +396,36 @@ async def test_when_hour_mode_is_minimum_and_minimum_or_maximum_value_is_specifi errors = validate_target_timeframe_config(data) # Assert - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_WEIGHTING not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys) + +@pytest.mark.asyncio +@pytest.mark.parametrize("min_value,max_value",[ + ("1.5","1.49"), + (1.5,1.49), + ("-1.49","-1.5"), + (-1.49,-1.5), +]) +async def test_when_minimum_value_greater_to_maximum_value_is_specified_then_error_returned(min_value: float, max_value: float): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_TARGET_START_TIME: "00:00", + CONFIG_TARGET_END_TIME: "00:00", + CONFIG_TARGET_OFFSET: "-00:30:00", + CONFIG_TARGET_MIN_VALUE: min_value, + CONFIG_TARGET_MAX_VALUE: max_value, + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_MINIMUM + } + + # Act + errors = validate_target_timeframe_config(data) + + # Assert + assert CONFIG_TARGET_MIN_VALUE in errors + assert errors[CONFIG_TARGET_MIN_VALUE] == "minimum_value_not_less_than_maximum_value" + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_MIN_VALUE) @pytest.mark.asyncio @pytest.mark.parametrize("hour_mode",[ @@ -523,15 +452,7 @@ async def test_when_hour_mode_is_not_exact_and_weighting_specified_then_error_re # Assert assert CONFIG_TARGET_WEIGHTING in errors assert errors[CONFIG_TARGET_WEIGHTING] == "weighting_not_supported_for_hour_mode" - - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_HOURS_MODE not in errors + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_WEIGHTING) @pytest.mark.asyncio async def test_when_hour_mode_is_minimum_and_minimum_and_maximum_value_is_not_specified_then_error_returned(): @@ -550,13 +471,101 @@ async def test_when_hour_mode_is_minimum_and_minimum_and_maximum_value_is_not_sp errors = validate_target_timeframe_config(data) # Assert - assert CONFIG_TARGET_NAME not in errors - assert CONFIG_TARGET_HOURS not in errors - assert CONFIG_TARGET_START_TIME not in errors - assert CONFIG_TARGET_END_TIME not in errors - assert CONFIG_TARGET_OFFSET not in errors - assert CONFIG_TARGET_MIN_VALUE not in errors - assert CONFIG_TARGET_MAX_VALUE not in errors - assert CONFIG_TARGET_WEIGHTING not in errors - assert CONFIG_TARGET_HOURS_MODE in errors - assert errors[CONFIG_TARGET_HOURS_MODE] == "minimum_or_maximum_value_not_specified" \ No newline at end of file + assert errors[CONFIG_TARGET_HOURS_MODE] == "minimum_or_maximum_value_not_specified" + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_HOURS_MODE) + +@pytest.mark.asyncio +@pytest.mark.parametrize("minimum_required_minutes",[ + ("30"), + (30), + ("1"), + (1), +]) +async def test_when_minimum_required_minutes_set_to_valid_integer_then_no_error_returned(minimum_required_minutes): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_TARGET_START_TIME: "00:00", + CONFIG_TARGET_END_TIME: "00:00", + CONFIG_TARGET_OFFSET: "-00:30:00", + CONFIG_TARGET_MIN_VALUE: "0", + CONFIG_TARGET_MAX_VALUE: "10", + CONFIG_TARGET_WEIGHTING: "2,*,2", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT, + CONFIG_TARGET_DANGEROUS_SETTINGS: { + CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: minimum_required_minutes + } + } + + # Act + errors = validate_target_timeframe_config(data) + + # Assert + assert_errors_not_present(errors, default_keys) + +@pytest.mark.asyncio +@pytest.mark.parametrize("minimum_required_minutes",[ + ("30.1"), + (30.1), + ("a"), +]) +async def test_when_minimum_required_minutes_set_to_invalid_integer_then_error_returned(minimum_required_minutes): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_TARGET_START_TIME: "00:00", + CONFIG_TARGET_END_TIME: "00:00", + CONFIG_TARGET_OFFSET: "-00:30:00", + CONFIG_TARGET_MIN_VALUE: "0", + CONFIG_TARGET_MAX_VALUE: "10", + CONFIG_TARGET_WEIGHTING: "2,*,2", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT, + CONFIG_TARGET_DANGEROUS_SETTINGS: { + CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: minimum_required_minutes + } + } + + # Act + errors = validate_target_timeframe_config(data) + + # Assert + assert CONFIG_TARGET_DANGEROUS_SETTINGS in errors + assert errors[CONFIG_TARGET_DANGEROUS_SETTINGS] == "invalid_integer" + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_DANGEROUS_SETTINGS) + +@pytest.mark.asyncio +@pytest.mark.parametrize("minimum_required_minutes",[ + ("0"), + (0), + ("31"), + (31), +]) +async def test_when_minimum_required_minutes_set_to_invalid_value_then_error_returned(minimum_required_minutes): + # Arrange + data = { + CONFIG_TARGET_TYPE: CONFIG_TARGET_TYPE_CONTINUOUS, + CONFIG_TARGET_NAME: "test", + CONFIG_TARGET_HOURS: "1.5", + CONFIG_TARGET_START_TIME: "00:00", + CONFIG_TARGET_END_TIME: "00:00", + CONFIG_TARGET_OFFSET: "-00:30:00", + CONFIG_TARGET_MIN_VALUE: "0", + CONFIG_TARGET_MAX_VALUE: "10", + CONFIG_TARGET_WEIGHTING: "2,*,2", + CONFIG_TARGET_HOURS_MODE: CONFIG_TARGET_HOURS_MODE_EXACT, + CONFIG_TARGET_DANGEROUS_SETTINGS: { + CONFIG_TARGET_MINIMUM_REQUIRED_MINUTES_IN_SLOT: minimum_required_minutes + } + } + + # Act + errors = validate_target_timeframe_config(data) + + # Assert + assert CONFIG_TARGET_DANGEROUS_SETTINGS in errors + assert errors[CONFIG_TARGET_DANGEROUS_SETTINGS] == "invalid_minimum_required_minutes_in_slot" + assert_errors_not_present(errors, default_keys, CONFIG_TARGET_DANGEROUS_SETTINGS) \ No newline at end of file diff --git a/tests/unit/target_rates/test_calculate_continuous_times.py b/tests/unit/target_rates/test_calculate_continuous_times.py index 195fbc1..caad802 100644 --- a/tests/unit/target_rates/test_calculate_continuous_times.py +++ b/tests/unit/target_rates/test_calculate_continuous_times.py @@ -6,57 +6,59 @@ from unit import (create_data_source_data, default_time_periods, values_to_thirty_minute_increments) from custom_components.target_timeframes.entities import calculate_continuous_times, get_fixed_applicable_time_periods, get_start_and_end_times +default_minimum_slot_minutes = 30 + @pytest.mark.asyncio -@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_valid_from,is_rolling_target,find_last_rates",[ - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), +@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_valid_from,minimum_slot_minutes,find_last_rates",[ + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), # No start set - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), # No end set - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T11:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), # No start or end set - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T14:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T20:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), ]) -async def test_when_continuous_times_present_then_next_continuous_times_returned(current_date, target_start_time, target_end_time, expected_first_valid_from, is_rolling_target, find_last_rates): +async def test_when_continuous_times_present_then_next_continuous_times_returned(current_date, target_start_time, target_end_time, expected_first_valid_from, minimum_slot_minutes, find_last_rates): # 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") @@ -71,7 +73,7 @@ async def test_when_continuous_times_present_then_next_continuous_times_returned # Restrict our time block target_hours = 1 - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, is_rolling_target) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -99,56 +101,56 @@ async def test_when_continuous_times_present_then_next_continuous_times_returned assert result[1]["value"] == 0.1 @pytest.mark.asyncio -@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_valid_from,is_rolling_target,find_last_rates",[ - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), +@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_valid_from,minimum_slot_minutes,find_last_rates",[ + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), # # No start set - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T17:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), # No end set - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T11:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), # No start or end set - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), ]) -async def test_when_continuous_times_present_and_highest_price_required_then_next_continuous_times_returned(current_date, target_start_time, target_end_time, expected_first_valid_from, is_rolling_target, find_last_rates): +async def test_when_continuous_times_present_and_highest_price_required_then_next_continuous_times_returned(current_date, target_start_time, target_end_time, expected_first_valid_from, minimum_slot_minutes, find_last_rates): # 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") @@ -163,7 +165,7 @@ async def test_when_continuous_times_present_and_highest_price_required_then_nex # Restrict our time block target_hours = 1 - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, is_rolling_target) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -191,24 +193,24 @@ async def test_when_continuous_times_present_and_highest_price_required_then_nex assert result[1]["value"] == 0.3 @pytest.mark.asyncio -@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), +@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_valid_from,minimum_slot_minutes",[ + (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"), None), + (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"), None), + (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"), 30), + (datetime.strptime("2023-01-01T23:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, None, 30), + + (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"), None), + (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"), None), + (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"), 30), + (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"), 30), + (datetime.strptime("2023-01-01T18:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), "05:00", "19:00", None, 30), + + (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"), None), + (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"), None), + (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"), 30), + (datetime.strptime("2023-01-02T05:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), "20:00", "06:00", None, 30), ]) -async def test_readme_examples(current_date, target_start_time, target_end_time, expected_first_valid_from, is_rolling_target): +async def test_readme_examples(current_date, target_start_time, target_end_time, expected_first_valid_from, minimum_slot_minutes): # Arrange values = values_to_thirty_minute_increments( [ @@ -275,7 +277,7 @@ async def test_readme_examples(current_date, target_start_time, target_end_time, # Restrict our time block target_hours = 1 - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, is_rolling_target) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -328,7 +330,7 @@ async def test_when_last_rate_is_currently_active_and_target_is_rolling_then_rat # Restrict our time block target_hours = 0.5 - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, True) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, default_minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -361,7 +363,7 @@ async def test_when_available_rates_are_too_low_then_no_times_are_returned(): # Restrict our time block target_hours = 3 - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -397,7 +399,7 @@ async def test_when_min_value_is_provided_then_result_does_not_include_any_rate_ [19.1, 18.9, 19.1, 20] ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, True) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, default_minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -443,7 +445,7 @@ async def test_when_max_value_is_provided_then_result_does_not_include_any_rate_ [19.1, 18.9, 19.1, 20] ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, True) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, default_minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -502,7 +504,7 @@ async def test_when_weighting_specified_then_result_is_adjusted(weighting: list, possible_values ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, True) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, default_minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -548,7 +550,7 @@ async def test_when_target_hours_zero_then_result_is_adjusted(weighting): [19.1, 18.9, 19.1, 15.1, 20] ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, True) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, default_minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -584,7 +586,7 @@ def test_when_hour_mode_is_maximum_and_not_enough_hours_available_then_reduced_t possible_values ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, True) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, default_minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -627,7 +629,7 @@ def test_when_hour_mode_is_maximum_and_more_than_enough_hours_available_then_tar possible_values ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, True) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, default_minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -668,7 +670,7 @@ def test_when_hour_mode_is_minimum_and_not_enough_hours_available_then_no_target possible_values ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, True) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, default_minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -704,7 +706,7 @@ def test_when_hour_mode_is_minimum_and_more_than_enough_hours_available_then_tar possible_values ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, True) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, default_minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, diff --git a/tests/unit/target_rates/test_calculate_intermittent_times.py b/tests/unit/target_rates/test_calculate_intermittent_times.py index c158307..488bfb0 100644 --- a/tests/unit/target_rates/test_calculate_intermittent_times.py +++ b/tests/unit/target_rates/test_calculate_intermittent_times.py @@ -6,58 +6,60 @@ from unit import (create_data_source_data, default_time_periods) from custom_components.target_timeframes.entities import calculate_intermittent_times, get_fixed_applicable_time_periods, get_start_and_end_times +default_minimum_slot_minutes = 30 + @pytest.mark.asyncio -@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_valid_from,is_rolling_target,find_last_rates",[ - (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, False), - (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, False), - (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, False), - (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"), False, False), - (datetime.strptime("2022-02-09T12: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"), False, False), - (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"), False, False), +@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_valid_from,minimum_slot_minutes,find_last_rates",[ + (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"), 30, False), + (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"), 30, False), + (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"), 30, False), + (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"), None, False), + (datetime.strptime("2022-02-09T12: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"), None, False), + (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"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), # # No start set - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), # # No end set - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), # # No start or end set - (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"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (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, 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, False), - - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (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"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (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"), None, 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"), None, False), + + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T21:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), ]) -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, find_last_rates): +async def test_when_intermittent_times_present_then_next_intermittent_times_returned(current_date, target_start_time, target_end_time, expected_first_valid_from, minimum_slot_minutes, find_last_rates): # 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") @@ -72,7 +74,7 @@ async def test_when_intermittent_times_present_then_next_intermittent_times_retu # Restrict our time block target_hours = 1 - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, is_rolling_target) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -100,56 +102,56 @@ async def test_when_intermittent_times_present_then_next_intermittent_times_retu assert result[1]["value"] == 0.1 @pytest.mark.asyncio -@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_valid_from,is_rolling_target,find_last_rates",[ - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T13:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), +@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_valid_from,minimum_slot_minutes,find_last_rates",[ + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T13:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), # # No start set - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T13:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T13:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), # # No end set - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T13:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T13:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), # # No start or end set - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T13:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T13:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, False), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, False), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, True), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, True), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, True), ]) -async def test_when_intermittent_times_present_and_highest_prices_are_true_then_next_intermittent_times_returned(current_date, target_start_time, target_end_time, expected_first_valid_from, is_rolling_target, find_last_rates): +async def test_when_intermittent_times_present_and_highest_prices_are_true_then_next_intermittent_times_returned(current_date, target_start_time, target_end_time, expected_first_valid_from, minimum_slot_minutes, find_last_rates): # 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") @@ -164,7 +166,7 @@ async def test_when_intermittent_times_present_and_highest_prices_are_true_then_ # Restrict our time block target_hours = 1 - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, is_rolling_target) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -210,7 +212,7 @@ async def test_when_current_time_has_not_enough_time_left_then_no_intermittent_t target_end_time = "18:00" target_hours = 1 - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, True) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, default_minimum_slot_minutes) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -238,7 +240,7 @@ async def test_when_using_agile_times_then_lowest_rates_are_picked(): # Restrict our time block target_hours = 3 - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -290,7 +292,7 @@ async def test_when_available_rates_are_too_low_then_no_times_are_returned(): # Restrict our time block target_hours = 3 - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -326,7 +328,7 @@ async def test_when_min_value_is_provided_then_result_does_not_include_any_rate_ [19.1, 18.9, 19.5, 17.9, 16.5, 20.1] ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -368,7 +370,7 @@ async def test_when_max_value_is_provided_then_result_does_not_include_any_rate_ [19.1, 18.9, 19.5, 17.9, 16.5, 20.1] ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -407,7 +409,7 @@ async def test_when_hour_mode_is_maximum_and_not_enough_hours_available_then_red [19.1, 18.9, 19.5, 18.9, 20.1, 18.9] ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -448,7 +450,7 @@ async def test_when_hour_mode_is_maximum_and_more_than_enough_hours_available_th [19.1, 18.9, 19.5, 18.9, 20.1, 18.9] ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -489,7 +491,7 @@ async def test_when_hour_mode_is_minimum_and_not_enough_hours_available_then_no_ [19.1, 18.9, 19.5, 18.9, 20.1, 18.9] ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, @@ -524,7 +526,7 @@ async def test_when_hour_mode_is_minimum_and_more_than_enough_hours_available_th [19.1, 18.9, 19.5, 18.9, 20.1, 18.9] ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) applicable_time_periods = get_fixed_applicable_time_periods( target_start_datetime, diff --git a/tests/unit/target_rates/test_get_fixed_applicable_time_periods.py b/tests/unit/target_rates/test_get_fixed_applicable_time_periods.py index 17a8193..0c152f3 100644 --- a/tests/unit/target_rates/test_get_fixed_applicable_time_periods.py +++ b/tests/unit/target_rates/test_get_fixed_applicable_time_periods.py @@ -4,40 +4,43 @@ from custom_components.target_timeframes.entities import get_fixed_applicable_time_periods, get_start_and_end_times from unit import create_data_source_data, default_time_periods, values_to_thirty_minute_increments +default_minimum_slot_minutes = 30 + @pytest.mark.asyncio -@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_valid_from,is_rolling_target,expected_number_of_rates",[ - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, 16), - (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, 12), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, 16), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, 16), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, 16), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, 16), +@pytest.mark.parametrize("current_date,target_start_time,target_end_time,expected_first_valid_from,minimum_slot_minutes,expected_number_of_rates",[ + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, 16), + (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"), 30, 12), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, 16), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, 16), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, 16), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", "18:00", datetime.strptime("2022-02-10T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, 16), # No start set - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, 36), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, 12), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, 36), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, 36), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, 36), - (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, 36), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, 36), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, 12), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, 36), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, 36), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, 36), + (datetime.strptime("2022-02-09T19:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, "18:00", datetime.strptime("2022-02-10T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, 36), # No end set - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, 28), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, 24), - (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, 28), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), False, 28), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, 28), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, 24), + (datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, 28), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), "10:00", None, datetime.strptime("2022-02-09T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, 28), # No start or end set - (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"), True, 48), - (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), True, 24), - (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, 48), - (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, 48), + (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"), 30, 48), + (datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), None, None, datetime.strptime("2022-02-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), 30, 24), + (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"), None, 48), + (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"), None, 48), ]) -async def test_when_continuous_times_present_then_next_continuous_times_returned(current_date, target_start_time, target_end_time, expected_first_valid_from, is_rolling_target, expected_number_of_rates): +async def test_when_continuous_times_present_then_next_continuous_times_returned(current_date, target_start_time, target_end_time, expected_first_valid_from, minimum_slot_minutes, expected_number_of_rates): # 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_values = [0.1, 20, 0.3, 20, 20, 0.1] + calculate_with_incomplete_data = False values = create_data_source_data( period_from, @@ -45,13 +48,14 @@ async def test_when_continuous_times_present_then_next_continuous_times_returned expected_values ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, is_rolling_target) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Act result = get_fixed_applicable_time_periods( target_start_datetime, target_end_datetime, - values + values, + calculate_with_incomplete_data ) assert result is not None @@ -68,16 +72,18 @@ async def test_when_start_time_is_after_end_time_then_rates_are_overnight(): 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" + calculate_with_incomplete_data = False expected_first_valid_from = datetime.strptime("2022-10-21T23:30:00+00:00", "%Y-%m-%dT%H:%M:%S%z") - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) # Act result = get_fixed_applicable_time_periods( target_start_datetime, target_end_datetime, - default_time_periods + default_time_periods, + calculate_with_incomplete_data ) # Assert @@ -96,16 +102,18 @@ async def test_when_start_time_and_end_time_is_same_then_rates_are_shifted(): 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" + calculate_with_incomplete_data = False expected_first_valid_from = datetime.strptime("2022-10-21T23:30:00+00:00", "%Y-%m-%dT%H:%M:%S%z") - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) # Act result = get_fixed_applicable_time_periods( target_start_datetime, target_end_datetime, - default_time_periods + default_time_periods, + calculate_with_incomplete_data ) # Assert @@ -122,6 +130,7 @@ async def test_when_start_time_is_after_end_time_and_rolling_target_then_rates_a 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" + calculate_with_incomplete_data = False expected_first_valid_from = datetime.strptime("2022-10-21T23:30:00+00:00", "%Y-%m-%dT%H:%M:%S%z") @@ -147,13 +156,14 @@ async def test_when_start_time_is_after_end_time_and_rolling_target_then_rates_a datetime.strptime("2022-10-24T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, True) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, default_minimum_slot_minutes) # Act result = get_fixed_applicable_time_periods( target_start_datetime, target_end_datetime, - values + values, + calculate_with_incomplete_data ) # Assert @@ -171,6 +181,7 @@ async def test_when_start_time_and_end_time_is_same_and_rolling_target_then_rate 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" + calculate_with_incomplete_data = False expected_first_valid_from = datetime.strptime("2022-10-22T02:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") @@ -201,13 +212,14 @@ async def test_when_start_time_and_end_time_is_same_and_rolling_target_then_rate datetime.strptime("2022-10-24T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, True) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, default_minimum_slot_minutes) # Act result = get_fixed_applicable_time_periods( target_start_datetime, target_end_datetime, - values + values, + calculate_with_incomplete_data ) # Assert @@ -226,25 +238,55 @@ async def test_when_available_rates_are_too_low_then_no_times_are_returned(): 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" + calculate_with_incomplete_data = False - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) # Act result = get_fixed_applicable_time_periods( target_start_datetime, target_end_datetime, - default_time_periods + default_time_periods, + calculate_with_incomplete_data ) # Assert assert result is None +@pytest.mark.asyncio +async def test_when_available_rates_are_too_low_and_incomplete_data_is_true_then_target_rates_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" + calculate_with_incomplete_data = True + + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) + + # Act + result = get_fixed_applicable_time_periods( + target_start_datetime, + target_end_datetime, + default_time_periods, + calculate_with_incomplete_data + ) + + # Assert + assert result is not None + assert len(result) == 14 + expected_first_valid_from = datetime.strptime("2022-10-22T16:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + for item in result: + assert item["start"] == expected_first_valid_from + assert item["end"] == expected_first_valid_from + timedelta(minutes=30) + expected_first_valid_from = item["end"] + @pytest.mark.asyncio async def test_when_times_are_in_bst_then_rates_are_shifted(): # Arrange current_date = datetime.strptime("2024-04-06T17:10:00+01:00", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "16:00" target_end_time = "21:00" + calculate_with_incomplete_data = False period_from = datetime.strptime("2024-04-06T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") period_to = datetime.strptime("2024-04-07T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") @@ -256,13 +298,14 @@ async def test_when_times_are_in_bst_then_rates_are_shifted(): expected_values ) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) # Act result = get_fixed_applicable_time_periods( target_start_datetime, target_end_datetime, - values + values, + calculate_with_incomplete_data ) # Assert @@ -282,6 +325,7 @@ async def test_when_clocks_go_back_then_correct_times_are_selected(): current_date = datetime.strptime("2024-10-27T10:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "23:00" target_end_time = "23:00" + calculate_with_incomplete_data = False values = [ { @@ -538,13 +582,14 @@ async def test_when_clocks_go_back_then_correct_times_are_selected(): values.sort(key=lambda x: (x["start"].timestamp(), x["start"].fold)) - (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, False) + (target_start_datetime, target_end_datetime) = get_start_and_end_times(current_date, target_start_time, target_end_time, None) # Act result = get_fixed_applicable_time_periods( target_start_datetime, target_end_datetime, - values + values, + calculate_with_incomplete_data ) # Assert diff --git a/tests/unit/target_rates/test_get_rolling_applicable_time_periods.py b/tests/unit/target_rates/test_get_rolling_applicable_time_periods.py index a17d727..7778307 100644 --- a/tests/unit/target_rates/test_get_rolling_applicable_time_periods.py +++ b/tests/unit/target_rates/test_get_rolling_applicable_time_periods.py @@ -10,9 +10,11 @@ async def test_when_rates_is_none_then_none_returned(): current_datetime = datetime.strptime("2024-10-19T10:15:00+01:00", "%Y-%m-%dT%H:%M:%S%z") all_rates = None target_hours = 2 + minimum_slot_minutes = 15 + calculate_with_incomplete_data = False # Act - actual_rates = get_rolling_applicable_time_periods(current_datetime, all_rates, target_hours) + actual_rates = get_rolling_applicable_time_periods(current_datetime, all_rates, target_hours, minimum_slot_minutes, calculate_with_incomplete_data) # Assert assert actual_rates is None @@ -27,13 +29,41 @@ async def test_when_not_enough_rates_available_then_none_returned(): [1] ) target_hours = 2 + minimum_slot_minutes = 15 + calculate_with_incomplete_data = False # Act - actual_rates = get_rolling_applicable_time_periods(current_datetime, all_rates, target_hours) + actual_rates = get_rolling_applicable_time_periods(current_datetime, all_rates, target_hours, minimum_slot_minutes, calculate_with_incomplete_data) # Assert assert actual_rates is None +@pytest.mark.asyncio +async def test_when_not_enough_rates_available_and_incomplete_data_is_true_then_target_rates_returned(): + # Arrange + current_datetime = datetime.strptime("2024-10-19T10:00:01+01:00", "%Y-%m-%dT%H:%M:%S%z") + all_rates = create_data_source_data( + datetime.strptime("2024-10-19T10:00:00+01:00", "%Y-%m-%dT%H:%M:%S%z"), + datetime.strptime("2024-10-19T11:00:00+01:00", "%Y-%m-%dT%H:%M:%S%z"), + [1] + ) + target_hours = 2 + minimum_slot_minutes = 15 + calculate_with_incomplete_data = True + + # Act + actual_rates = get_rolling_applicable_time_periods(current_datetime, all_rates, target_hours, minimum_slot_minutes, calculate_with_incomplete_data) + + # Assert + assert actual_rates is not None + assert len(actual_rates) == 2 + expected_start_date = datetime.strptime("2024-10-19T10:00:00+01:00", "%Y-%m-%dT%H:%M:%S%z") + for rate in actual_rates: + assert rate["start"] == expected_start_date + + expected_start_date += timedelta(minutes=30) + assert rate["end"] == expected_start_date + @pytest.mark.asyncio async def test_when_rates_available_then_target_rates_returned(): # Arrange @@ -44,9 +74,63 @@ async def test_when_rates_available_then_target_rates_returned(): [1, 2, 3] ) target_hours = 2 + minimum_slot_minutes = 15 + calculate_with_incomplete_data = False + + # Act + actual_rates = get_rolling_applicable_time_periods(current_datetime, all_rates, target_hours, minimum_slot_minutes, calculate_with_incomplete_data) + + # Assert + assert actual_rates is not None + assert len(actual_rates) == 4 + expected_start_date = datetime.strptime("2024-10-19T10:00:00+01:00", "%Y-%m-%dT%H:%M:%S%z") + for rate in actual_rates: + assert rate["start"] == expected_start_date + + expected_start_date += timedelta(minutes=30) + assert rate["end"] == expected_start_date + +@pytest.mark.asyncio +async def test_when_current_time_period_has_less_than_minimum_minutes_then_target_rates_returned_starting_at_next_time_period(): + # Arrange + current_datetime = datetime.strptime("2024-10-19T10:15:00+01:00", "%Y-%m-%dT%H:%M:%S%z") + all_rates = create_data_source_data( + datetime.strptime("2024-10-19T00:00:00+01:00", "%Y-%m-%dT%H:%M:%S%z"), + datetime.strptime("2024-10-20T00:00:00+01:00", "%Y-%m-%dT%H:%M:%S%z"), + [1, 2, 3] + ) + target_hours = 2 + minimum_slot_minutes = 29 + calculate_with_incomplete_data = False + + # Act + actual_rates = get_rolling_applicable_time_periods(current_datetime, all_rates, target_hours, minimum_slot_minutes, calculate_with_incomplete_data) + + # Assert + assert actual_rates is not None + assert len(actual_rates) == 4 + expected_start_date = datetime.strptime("2024-10-19T10:30:00+01:00", "%Y-%m-%dT%H:%M:%S%z") + for rate in actual_rates: + assert rate["start"] == expected_start_date + + expected_start_date += timedelta(minutes=30) + assert rate["end"] == expected_start_date + +@pytest.mark.asyncio +async def test_when_current_time_period_has_more_than_minimum_minutes_then_target_rates_returned_starting_at_current_time_period(): + # Arrange + current_datetime = datetime.strptime("2024-10-19T10:15:00+01:00", "%Y-%m-%dT%H:%M:%S%z") + all_rates = create_data_source_data( + datetime.strptime("2024-10-19T00:00:00+01:00", "%Y-%m-%dT%H:%M:%S%z"), + datetime.strptime("2024-10-20T00:00:00+01:00", "%Y-%m-%dT%H:%M:%S%z"), + [1, 2, 3] + ) + target_hours = 2 + minimum_slot_minutes = 15 + calculate_with_incomplete_data = False # Act - actual_rates = get_rolling_applicable_time_periods(current_datetime, all_rates, target_hours) + actual_rates = get_rolling_applicable_time_periods(current_datetime, all_rates, target_hours, minimum_slot_minutes, calculate_with_incomplete_data) # Assert assert actual_rates is not None diff --git a/tests/unit/target_rates/test_get_start_and_end_times.py b/tests/unit/target_rates/test_get_start_and_end_times.py index ea373f6..b0972d9 100644 --- a/tests/unit/target_rates/test_get_start_and_end_times.py +++ b/tests/unit/target_rates/test_get_start_and_end_times.py @@ -12,10 +12,10 @@ async def test_when_target_times_are_none_and_start_time_not_in_past_is_false_th current_date = datetime.strptime("2023-11-16T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = None target_end_time = None - start_time_not_in_past = False + minimum_slot_minutes = None # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -30,10 +30,10 @@ async def test_when_target_times_are_none_and_start_time_not_in_past_is_true_the current_date = datetime.strptime("2023-11-16T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = None target_end_time = None - start_time_not_in_past = True + minimum_slot_minutes = 30 # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -48,10 +48,10 @@ async def test_when_start_time_provided_and_start_time_not_in_past_is_true_then_ current_date = datetime.strptime("2023-11-16T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "10:00" target_end_time = None - start_time_not_in_past = True + minimum_slot_minutes = 30 # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -66,10 +66,10 @@ async def test_when_start_time_provided_and_start_time_not_in_past_is_false_then current_date = datetime.strptime("2023-11-16T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "10:00" target_end_time = None - start_time_not_in_past = False + minimum_slot_minutes = None # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -84,10 +84,10 @@ async def test_when_end_time_provided_and_start_time_not_in_past_is_true_then_re current_date = datetime.strptime("2023-11-16T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = None target_end_time = "18:00" - start_time_not_in_past = True + minimum_slot_minutes = 30 # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -102,10 +102,10 @@ async def test_when_end_time_provided_and_start_time_not_in_past_is_false_then_r current_date = datetime.strptime("2023-11-16T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = None target_end_time = "18:00" - start_time_not_in_past = False + minimum_slot_minutes = None # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -120,10 +120,10 @@ async def test_when_target_times_provided_and_start_time_not_in_past_is_false_th current_date = datetime.strptime("2023-11-16T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "10:00" target_end_time = "15:00" - start_time_not_in_past = False + minimum_slot_minutes = None # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -138,10 +138,10 @@ async def test_when_target_times_provided_and_start_time_not_in_past_is_true_the current_date = datetime.strptime("2023-11-16T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "10:00" target_end_time = "15:00" - start_time_not_in_past = True + minimum_slot_minutes = 30 # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -156,10 +156,10 @@ async def test_when_start_time_after_end_time_and_start_after_current_and_start_ current_date = datetime.strptime("2023-11-16T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "22:00" target_end_time = "04:00" - start_time_not_in_past = True + minimum_slot_minutes = 30 # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -174,10 +174,10 @@ async def test_when_start_time_after_end_time_and_start_before_current_and_start current_date = datetime.strptime("2023-11-16T23:30:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "22:00" target_end_time = "04:00" - start_time_not_in_past = False + minimum_slot_minutes = None # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T22:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -192,10 +192,10 @@ async def test_when_start_time_after_end_time_and_start_before_current_and_start current_date = datetime.strptime("2023-11-16T23:30:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "22:00" target_end_time = "04:00" - start_time_not_in_past = True + minimum_slot_minutes = 30 # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T23:30:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -205,15 +205,15 @@ async def test_when_start_time_after_end_time_and_start_before_current_and_start assert target_end == expected_end @pytest.mark.asyncio -async def test_when_rolling_target_and_start_time_in_past_and_end_time_in_future_then_start_becomes_current_time(): +async def test_when_minimum_slot_minutes_specified_and_start_time_in_past_and_end_time_in_future_then_start_becomes_current_time(): # Arrange current_date = datetime.strptime("2023-11-16T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "10:00" target_end_time = "15:00" - start_time_not_in_past = True + minimum_slot_minutes = 30 # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = current_date @@ -223,15 +223,15 @@ async def test_when_rolling_target_and_start_time_in_past_and_end_time_in_future assert target_end == expected_end @pytest.mark.asyncio -async def test_when_non_rolling_target_and_start_time_in_past_and_end_time_in_future_then_start_stays_in_past(): +async def test_when_no_minimum_slot_minutes_and_start_time_in_past_and_end_time_in_future_then_start_stays_in_past(): # Arrange current_date = datetime.strptime("2023-11-16T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "10:00" target_end_time = "15:00" - start_time_not_in_past = False + minimum_slot_minutes = None # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -246,10 +246,10 @@ async def test_when_both_start_and_end_times_in_past_then_moves_to_next_day(): current_date = datetime.strptime("2023-11-16T16:00:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "10:00" target_end_time = "15:00" - start_time_not_in_past = True + minimum_slot_minutes = 30 # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-17T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -259,15 +259,15 @@ async def test_when_both_start_and_end_times_in_past_then_moves_to_next_day(): assert target_end == expected_end @pytest.mark.asyncio -async def test_when_times_are_same_day_with_current_between_them_and_non_rolling_target(): +async def test_when_times_are_same_day_with_current_between_them_and_no_minimum_slot_minutes(): # Arrange current_date = datetime.strptime("2023-11-16T12:30:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "10:00" target_end_time = "15:00" - start_time_not_in_past = False + minimum_slot_minutes = None # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -282,14 +282,51 @@ async def test_when_current_is_before_both_start_and_end_times_same_day(): current_date = datetime.strptime("2023-11-16T08:00:00Z", "%Y-%m-%dT%H:%M:%S%z") target_start_time = "10:00" target_end_time = "15:00" - start_time_not_in_past = True + minimum_slot_minutes = 30 # Act - target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, start_time_not_in_past) + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) # Assert expected_start = datetime.strptime("2023-11-16T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") expected_end = datetime.strptime("2023-11-16T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert target_start == expected_start + assert target_end == expected_end + + +@pytest.mark.asyncio +async def test_when_current_is_after_start_and_above_minimum_slot_minutes_present_then_start_of_slot_returned(): + # Arrange + current_date = datetime.strptime("2023-11-16T10:10:20Z", "%Y-%m-%dT%H:%M:%S%z") + target_start_time = "10:00" + target_end_time = "15:00" + minimum_slot_minutes = 20 + + # Act + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) + + # Assert + expected_start = datetime.strptime("2023-11-16T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + expected_end = datetime.strptime("2023-11-16T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + + assert target_start == expected_start + assert target_end == expected_end + +@pytest.mark.asyncio +async def test_when_current_is_after_start_and_below_minimum_slot_minutes_present_then_start_of_next_slot_returned(): + # Arrange + current_date = datetime.strptime("2023-11-16T10:11:30Z", "%Y-%m-%dT%H:%M:%S%z") + target_start_time = "10:00" + target_end_time = "15:00" + minimum_slot_minutes = 20 + + # Act + target_start, target_end = get_start_and_end_times(current_date, target_start_time, target_end_time, minimum_slot_minutes) + + # Assert + expected_start = datetime.strptime("2023-11-16T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + expected_end = datetime.strptime("2023-11-16T15:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert target_start == expected_start assert target_end == expected_end \ No newline at end of file