From 7a81942efdeb11d8da1e6ca9593d898867410f8d Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Sun, 1 Jun 2025 05:33:56 +0100 Subject: [PATCH 1/7] docs: Updated example --- README.md | 2 +- _docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f345dc..6fefcc2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Target timeframes was a feature that has been extracted out of the [Octopus Energy integration](https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy). The idea is you can configure binary sensors that will find and turn on during the most optimal time periods based on external data sources, targeting either the lowest or highest values. What these values represent can be anything. In the original integration, the values represented cost of energy, and so the cheapest periods were discovered. But it could represent other things like -* Temperature to turn on sprinklers during the hottest times of the day +* Energy prices to turn on devices when cost is the cheapest * Carbon emissions to turn on devices when renewables on the grid are at their highest * Solar generation to turn on devices when the most energy is being generated. diff --git a/_docs/index.md b/_docs/index.md index 837b4dc..587a688 100644 --- a/_docs/index.md +++ b/_docs/index.md @@ -2,7 +2,7 @@ Target timeframes was a feature that has been extracted out of the [Octopus Energy integration](https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy). The idea is you can configure binary sensors that will find and turn on during the most optimal time periods based on external data sources, targeting either the lowest or highest values. What these values represent can be anything. In the original integration, the values represented cost of energy, and so the cheapest periods were discovered. But it could represent other things like -* Temperature to turn on sprinklers during the hottest times of the day +* Energy prices to turn on devices when cost is the cheapest * Carbon emissions to turn on devices when renewables on the grid are at their highest * Solar generation to turn on devices when the most energy is being generated. From fe3b46eee45ae37c8b1aedfd85f24c4f502307a4 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Sat, 7 Jun 2025 12:54:56 +0100 Subject: [PATCH 2/7] blueprint: Updated calculation for OE and CI rates to favour low rates --- ...rames_octopus_energy_carbon_intensity.yaml | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml b/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml index 754f6e6..bf6b7d9 100644 --- a/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml +++ b/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml @@ -14,16 +14,6 @@ blueprint: - sensor integration: target_timeframes multiple: false - octopus_energy_previous_day_rates: - name: Previous day rates - description: The previous day rates event sensor supplied by Octopus Energy. More information can be found at https://bottlecapdave.github.io/HomeAssistant-OctopusEnergy/entities/electricity/#previous-day-rates. - selector: - entity: - filter: - - domain: - - event - integration: octopus_energy - multiple: false octopus_energy_current_day_rates: name: Current day rates description: The current day rates event sensor supplied by Octopus Energy. More information can be found at https://bottlecapdave.github.io/HomeAssistant-OctopusEnergy/entities/electricity/#current-day-rates. @@ -61,6 +51,13 @@ blueprint: selector: number: mode: box + octopus_energy_weighting: + name: Octopus Energy rate weighting + description: The weighting to apply to the Octopus Energy rates when calculating the final value for the period. + default: 0.7 + selector: + number: + mode: box carbon_intensity_current_day_rates: name: Current day rates description: The current day rates event sensor supplied by Carbon Intensity. More information can be found at https://bottlecapdave.github.io/HomeAssistant-CarbonIntensity/entities/#current-day-rates. @@ -81,20 +78,26 @@ blueprint: - event integration: carbon_intensity multiple: false + carbon_intensity_weighting: + name: Carbon Intensity forecast weighting + description: The weighting to apply to the Carbon Intensity forecast when calculating the final value for the period. + default: 0.3 + selector: + number: + mode: box variables: target_timeframe_data_source_sensor: !input target_timeframe_data_source_sensor - octopus_energy_previous_day_rates: !input octopus_energy_previous_day_rates octopus_energy_current_day_rates: !input octopus_energy_current_day_rates octopus_energy_next_day_rates: !input octopus_energy_next_day_rates octopus_energy_free_electricity: !input octopus_energy_free_electricity octopus_energy_free_electricity_weighting: !input octopus_energy_free_electricity_weighting carbon_intensity_current_day_rates: !input carbon_intensity_current_day_rates carbon_intensity_next_day_rates: !input carbon_intensity_next_day_rates + octopus_energy_weighting: !input octopus_energy_weighting + carbon_intensity_weighting: !input carbon_intensity_weighting mode: queued max: 4 triggers: -- platform: state - entity_id: !input octopus_energy_previous_day_rates - platform: state entity_id: !input octopus_energy_current_day_rates - platform: state @@ -110,9 +113,6 @@ action: - action: target_timeframes.update_target_timeframe_data_source data: > {%- set all_oe_rates = [] -%} - {%- if state_attr(octopus_energy_previous_day_rates, 'rates') != None -%} - {%- set all_oe_rates = all_oe_rates + state_attr(octopus_energy_previous_day_rates, 'rates') -%} - {%- endif -%} {%- if state_attr(octopus_energy_current_day_rates, 'rates') != None -%} {%- set all_oe_rates = all_oe_rates + state_attr(octopus_energy_current_day_rates, 'rates') -%} {%- endif -%} @@ -132,11 +132,16 @@ action: {%- set all_ci_rates = all_ci_rates + state_attr(carbon_intensity_next_day_rates, 'rates') -%} {%- endif -%} + {%- set min_rate = all_oe_rates | map(attribute='value_inc_vat') | min -%} + {%- set max_rate = all_oe_rates | map(attribute='value_inc_vat') | max -%} + {%- set min_carbon = all_ci_rates | map(attribute='intensity_forecast') | min -%} + {%- set max_carbon = all_ci_rates | map(attribute='intensity_forecast') | max -%} + {%- set data = namespace(new_rates=[]) -%} {%- for rate in all_oe_rates -%} {%- set start = rate["start"] | as_timestamp | timestamp_utc -%} {%- set end = rate["end"] | as_timestamp | timestamp_utc -%} - {%- set value = rate["value_inc_vat"] | float -%} + {%- set value = 1 - ((rate["value_inc_vat"] | float - min_rate) / (max_rate - min_rate) * octopus_energy_weighting) -%} {%- set free_namespace = namespace(is_free=False) -%} {%- for free_session in free_electricity_rates -%} @@ -158,7 +163,7 @@ action: {%- set metadata = { "rate": rate["value_inc_vat"], "is_capped": rate["is_capped"] } -%} {%- if carbon_intensity_namespace.rate -%} - {%- set value = value * (carbon_intensity_namespace.rate["intensity_forecast"] | float) -%} + {%- set value = value + (1 - ((carbon_intensity_namespace.rate["intensity_forecast"] | float - min_carbon) / (max_rate - min_carbon) * carbon_intensity_weighting)) -%} {%- set metadata = dict(metadata.items(), carbon_intensity=carbon_intensity_namespace.rate["intensity_forecast"] | float) -%} {%- endif -%} From 4ca7483c591e9149e808dcc53b127d019ad865c9 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Sat, 7 Jun 2025 12:57:04 +0100 Subject: [PATCH 3/7] chore: Fixed blueprint --- .../target_timeframes_octopus_energy_carbon_intensity.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml b/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml index bf6b7d9..b333183 100644 --- a/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml +++ b/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml @@ -163,7 +163,7 @@ action: {%- set metadata = { "rate": rate["value_inc_vat"], "is_capped": rate["is_capped"] } -%} {%- if carbon_intensity_namespace.rate -%} - {%- set value = value + (1 - ((carbon_intensity_namespace.rate["intensity_forecast"] | float - min_carbon) / (max_rate - min_carbon) * carbon_intensity_weighting)) -%} + {%- set value = value + (1 - ((carbon_intensity_namespace.rate["intensity_forecast"] | float - min_carbon) / (max_carbon - min_carbon) * carbon_intensity_weighting)) -%} {%- set metadata = dict(metadata.items(), carbon_intensity=carbon_intensity_namespace.rate["intensity_forecast"] | float) -%} {%- endif -%} From 78160175ac9a6165235576a4bc6003881f9c72b7 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Sun, 8 Jun 2025 03:00:35 +0100 Subject: [PATCH 4/7] chore: Fixed blueprint when rates don't differ between days --- ...target_timeframes_octopus_energy_carbon_intensity.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml b/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml index b333183..4c6fb19 100644 --- a/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml +++ b/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml @@ -136,13 +136,15 @@ action: {%- set max_rate = all_oe_rates | map(attribute='value_inc_vat') | max -%} {%- set min_carbon = all_ci_rates | map(attribute='intensity_forecast') | min -%} {%- set max_carbon = all_ci_rates | map(attribute='intensity_forecast') | max -%} + {%- set rate_diff = max_rate - min_rate if max_rate - min_rate != 0 else 1 %} + {%- set carbon_diff = max_carbon - min_carbon if max_carbon - min_carbon != 0 else 1 %} {%- set data = namespace(new_rates=[]) -%} {%- for rate in all_oe_rates -%} {%- set start = rate["start"] | as_timestamp | timestamp_utc -%} {%- set end = rate["end"] | as_timestamp | timestamp_utc -%} - {%- set value = 1 - ((rate["value_inc_vat"] | float - min_rate) / (max_rate - min_rate) * octopus_energy_weighting) -%} - + {%- set value = 1 - (((rate["value_inc_vat"] | float - min_rate) / rate_diff) * octopus_energy_weighting) -%} + {%- set free_namespace = namespace(is_free=False) -%} {%- for free_session in free_electricity_rates -%} {%- set free_start = free_session["start"] | as_timestamp | timestamp_utc -%} @@ -163,7 +165,7 @@ action: {%- set metadata = { "rate": rate["value_inc_vat"], "is_capped": rate["is_capped"] } -%} {%- if carbon_intensity_namespace.rate -%} - {%- set value = value + (1 - ((carbon_intensity_namespace.rate["intensity_forecast"] | float - min_carbon) / (max_carbon - min_carbon) * carbon_intensity_weighting)) -%} + {%- set value = value + (1 - (((carbon_intensity_namespace.rate["intensity_forecast"] | float - min_carbon) / carbon_diff) * carbon_intensity_weighting)) -%} {%- set metadata = dict(metadata.items(), carbon_intensity=carbon_intensity_namespace.rate["intensity_forecast"] | float) -%} {%- endif -%} From 127d3ad52ac766260373741d406cf0625d6b651d Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Sun, 8 Jun 2025 09:48:36 +0100 Subject: [PATCH 5/7] chore: More fixes to OE and Carbon intensity blueprint --- .../target_timeframes_octopus_energy_carbon_intensity.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml b/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml index 4c6fb19..3d8ae5c 100644 --- a/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml +++ b/_docs/blueprints/target_timeframes_octopus_energy_carbon_intensity.yaml @@ -143,7 +143,7 @@ action: {%- for rate in all_oe_rates -%} {%- set start = rate["start"] | as_timestamp | timestamp_utc -%} {%- set end = rate["end"] | as_timestamp | timestamp_utc -%} - {%- set value = 1 - (((rate["value_inc_vat"] | float - min_rate) / rate_diff) * octopus_energy_weighting) -%} + {%- set value = (((rate["value_inc_vat"] | float - min_rate) / rate_diff) * octopus_energy_weighting) -%} {%- set free_namespace = namespace(is_free=False) -%} {%- for free_session in free_electricity_rates -%} @@ -165,7 +165,7 @@ action: {%- set metadata = { "rate": rate["value_inc_vat"], "is_capped": rate["is_capped"] } -%} {%- if carbon_intensity_namespace.rate -%} - {%- set value = value + (1 - (((carbon_intensity_namespace.rate["intensity_forecast"] | float - min_carbon) / carbon_diff) * carbon_intensity_weighting)) -%} + {%- set value = value + (((carbon_intensity_namespace.rate["intensity_forecast"] | float - min_carbon) / carbon_diff) * carbon_intensity_weighting) -%} {%- set metadata = dict(metadata.items(), carbon_intensity=carbon_intensity_namespace.rate["intensity_forecast"] | float) -%} {%- endif -%} From 4ff880ebda435371c3e55cc86e4440f8e29a7f89 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Sun, 8 Jun 2025 13:20:17 +0100 Subject: [PATCH 6/7] feat: Added data source attribute to sensors --- .../target_timeframes/entities/rolling_target_timeframe.py | 2 ++ .../target_timeframes/entities/target_timeframe.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/custom_components/target_timeframes/entities/rolling_target_timeframe.py b/custom_components/target_timeframes/entities/rolling_target_timeframe.py index 5a32db8..abf645a 100644 --- a/custom_components/target_timeframes/entities/rolling_target_timeframe.py +++ b/custom_components/target_timeframes/entities/rolling_target_timeframe.py @@ -68,6 +68,7 @@ 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: @@ -205,6 +206,7 @@ async def async_update(self): self._attributes["next_average_value"] = active_result["next_average_value"] self._attributes["next_min_value"] = active_result["next_min_value"] self._attributes["next_max_value"] = active_result["next_max_value"] + self._attributes["data_source_id"] = self._data_source_id self._state = active_result["is_active"] diff --git a/custom_components/target_timeframes/entities/target_timeframe.py b/custom_components/target_timeframes/entities/target_timeframe.py index 9b9b5e0..5472525 100644 --- a/custom_components/target_timeframes/entities/target_timeframe.py +++ b/custom_components/target_timeframes/entities/target_timeframe.py @@ -73,6 +73,7 @@ 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: @@ -86,6 +87,7 @@ def __init__(self, hass: HomeAssistant, data_source_id: str, config_entry, confi self._data_source_data = initial_data if initial_data is not None else [] self._target_timeframes = [] + self._hass = hass self.entity_id = generate_entity_id("binary_sensor.{}", self.unique_id, hass=hass) @@ -230,6 +232,7 @@ async def async_update(self): self._attributes["next_average_value"] = active_result["next_average_value"] self._attributes["next_min_value"] = active_result["next_min_value"] self._attributes["next_max_value"] = active_result["next_max_value"] + self._attributes["data_source_id"] = self._data_source_id self._state = active_result["is_active"] From 54f68ca6fa2c5f4d65ed0c899aea87fc5afc1aa9 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Wed, 11 Jun 2025 17:04:16 +0100 Subject: [PATCH 7/7] feat: Updated update config service to re-evaluate straight away (5 minutes dev time) --- _docs/services.md | 8 ++++++++ .../entities/rolling_target_timeframe.py | 1 + .../target_timeframes/entities/target_timeframe.py | 1 + 3 files changed, 10 insertions(+) diff --git a/_docs/services.md b/_docs/services.md index 2666467..7ab07c2 100644 --- a/_docs/services.md +++ b/_docs/services.md @@ -54,6 +54,10 @@ The following services are available if you have set up at least one [target tim For updating a given [target timeframe's](./setup/target_timeframe.md) config. This allows you to change target timeframes sensors dynamically based on other outside criteria (e.g. you need to adjust the target hours to top up home batteries). +!!! warning + + This will cause the sensor to re-evaluate the target times, which may result in different times being picked. + | Attribute | Optional | Description | | ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------- | | `target.entity_id` | `no` | The name of the target sensor whose configuration is to be updated. | @@ -126,6 +130,10 @@ The following services are available if you have set up at least one [rolling ta For updating a given [rolling target timeframe's](./setup/rolling_target_timeframe.md) config. This allows you to change rolling target timeframes sensors dynamically based on other outside criteria (e.g. you need to adjust the target hours to top up home batteries). +!!! warning + + This will cause the sensor to re-evaluate the target times, which may result in different times being picked. + | Attribute | Optional | Description | | ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------- | | `target.entity_id` | `no` | The name of the target sensor whose configuration is to be updated. | diff --git a/custom_components/target_timeframes/entities/rolling_target_timeframe.py b/custom_components/target_timeframes/entities/rolling_target_timeframe.py index abf645a..9dd2af7 100644 --- a/custom_components/target_timeframes/entities/rolling_target_timeframe.py +++ b/custom_components/target_timeframes/entities/rolling_target_timeframe.py @@ -297,6 +297,7 @@ async def async_update_rolling_target_timeframe_config(self, target_hours=None, self._config = config self._attributes = self._config.copy() self._target_timeframes = [] + await self.async_update() self.async_write_ha_state() if persist_changes: diff --git a/custom_components/target_timeframes/entities/target_timeframe.py b/custom_components/target_timeframes/entities/target_timeframe.py index 5472525..cda5392 100644 --- a/custom_components/target_timeframes/entities/target_timeframe.py +++ b/custom_components/target_timeframes/entities/target_timeframe.py @@ -329,6 +329,7 @@ async def async_update_target_timeframe_config(self, target_start_time=None, tar self._config = config self._attributes = self._config.copy() self._target_timeframes = [] + await self.async_update() self.async_write_ha_state() if persist_changes: