From d40250d9a657947021e95c0291337b530532d2c8 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Sat, 11 Mar 2023 21:56:37 +0000 Subject: [PATCH] feat: Added service for updating a target rate's config temporarily --- README.md | 197 +++++++++++++++++- .../binary_sensors/target_rate.py | 67 +++++- .../carbon_intensity/services.yaml | 32 +++ 3 files changed, 287 insertions(+), 9 deletions(-) create mode 100644 custom_components/carbon_intensity/services.yaml diff --git a/README.md b/README.md index aa7e9fa..bda4af3 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,36 @@ Custom component to use the data from [carbonintensity.org.uk](https://carbonintensity.org.uk) to make your home more energy efficient. +- [Home Assistant Carbon Intensity](#home-assistant-carbon-intensity) + - [How to install](#how-to-install) + - [HACS](#hacs) + - [Manual](#manual) + - [How to setup](#how-to-setup) + - [Your Settings](#your-settings) + - [Target Rates](#target-rates) + - [From and To times](#from-and-to-times) + - [Offset](#offset) + - [Rolling Target](#rolling-target) + - [Examples](#examples) + - [Continuous](#continuous) + - [Intermittent](#intermittent) + - [Services](#services) + - [Service carbon\_intensity.update\_target\_config](#service-carbon_intensityupdate_target_config) + - [FAQ](#faq) + - [I'm having issues with the integration](#im-having-issues-with-the-integration) + ## How to install You should take the latest published [release](https://github.com/BottlecapDave/HomeAssistant-CarbonIntensity/releases). The current state of `develop` will be in flux and therefore possibly subject to change. -To install, place the contents of `custom_components` into the `/custom_components` folder of your Home Assistant installation. - ### HACS While the integration isn't available in the HACS store yet, you can install it as a [custom repository](https://hacs.xyz/docs/faq/custom_repositories), providing the url `https://github.com/BottlecapDave/HomeAssistant-CarbonIntensity` and category of `integration`. Once installed, don't forget to restart your home assistant instance for the integration to be picked up. +### Manual + +To install, place the contents of `custom_components` into the `/custom_components` folder of your Home Assistant installation. + ## How to setup Setup is done entirely via the [integration UI](https://my.home-assistant.io/redirect/config_flow_start/?domain=carbon_intensity). @@ -26,25 +46,186 @@ After completing, you'll get the following sensors: ### Target Rates -If you go through the [setup](https://my.home-assistant.io/redirect/config_flow_start/?domain=octopus_energy) process after you've configured your account, you can set up target rate sensors. These sensors calculate the lowest continuous or intermittent forecast intensity and turn on when these periods are active. These sensors can then be used in automations to turn on/off devices during times when the grid is more viable (either due to demand or available energy). +If you go through the [setup](https://my.home-assistant.io/redirect/config_flow_start/?domain=carbon_intensity) process after you've configured your setup, you can set up target rate sensors. These sensors calculate the lowest continuous or intermittent rates **within a 24 hour period** and turn on when these periods are active. These sensors can then be used in automations to turn on/off devices to reduce intensity on the grid and reduce carbon output. Each sensor will be in the form `binary_sensor.carbon_intensity_target_{{TARGET_RATE_NAME}}`. -#### Minimum and Maximum times +#### From and To times -If you're wanting your devices to come on during a certain period, you can set the minimum and maximum times in your target rate sensor. These are specified in 24 hour clock format and will attempt to find the lowest intensity forecasts during these times. +If you're wanting your devices to come on during a certain period, for example while you're at work, you can set the minimum and/or maximum times for your target rate sensor. These are specified in 24 hour clock format and will attempt to find the optimum discovered period during these times. + +If not specified, these default from `00:00:00` to `23:59:59`. However you can use this feature to change this evaluation period. + +If for example you want to look at rates overnight you could set your from time to something like `20:00` and your `to` time to something like `05:00`. If you're wanting to "shift" the evaluation period, you could set your `from` and `to` to something like `16:00`. + +See the examples below for how this might work. #### Offset -You may want your target rate sensors to turn on a period of time before optimum discovered period. For instance, you may be turning on a robot vacuum cleaner and want it to charge during the optimum period. For this, you'd use the `offset` field, which can be both positive and negative and go up to a maximum of 24 hours. +You may want your target rate sensors to turn on a period of time before the optimum discovered period. For example, you may be turning on a robot vacuum cleaner for a 30 minute clean and want it to charge during the optimum period. For this, you'd use the `offset` field and set it to `-00:30:00`, which can be both positive and negative and go up to a maximum of 24 hours. #### Rolling Target Depending on how you're going to use the sensor, you might want the best period to be found throughout the day so it's always applicable. For example, you might be using the sensor to turn on a washing machine which you might want to come on at the best time regardless of when you use the washing machine. -However, you might only want the target time to occur once a day so once the best time for that day has passed it won't turn on again. For example, you might be using the sensor to turn on something that isn't time critical and could wait till the next day like a charger. +However, you might also only want the target time to occur once a day so once the best time for that day has passed it won't turn on again. For example, you might be using the sensor to turn on something that isn't time critical and could wait till the next day like a charger. + +This feature is toggled on by the `Re-evaluate multiple times a day` checkbox. + +#### Examples + +Lets look at a few examples. Lets say we have the the following (unrealistic) set of intensity values. + +| start | end | value | +| ----- | --- | ----- | +| `2023-01-01T00:00` | `2023-01-01T00:30` | 6 | +| `2023-01-01T00:30` | `2023-01-01T05:00` | 12 | +| `2023-01-01T05:00` | `2023-01-01T05:30` | 7 | +| `2023-01-01T05:30` | `2023-01-01T18:00` | 20 | +| `2023-01-01T18:00` | `2023-01-01T23:30` | 34 | +| `2023-01-01T23:30` | `2023-01-02T00:30` | 5 | +| `2023-01-02T00:30` | `2023-01-02T05:00` | 12 | +| `2023-01-02T05:00` | `2023-01-02T05:30` | 7 | +| `2023-01-02T05:30` | `2023-01-02T18:00` | 20 | +| `2023-01-02T18:00` | `2023-01-02T23:00` | 34 | +| `2023-01-02T23:30` | `2023-01-03T00:00` | 6 | + +##### Continuous + +If we look at a continuous sensor that we want on for 1 hour. + +If we set no from/to times, then our 24 hour period being looked at ranges from `00:00:00` to `23:59:59`. + +The following table shows what this would be like. + +| current date/time | period | `Re-evaluate multiple times a day` | reasoning | +| ------------------ | ------------------------------------- | ---------------------------------- | --------- | +| `2023-01-01T00:00` | `2023-01-01T00:00` - `2023-01-01T01:00` | `false` | while 5 is our lowest rate within the current 24 hour period, it doesn't cover our whole 1 hour and is next to a high 34 rate. A rate of 6 is the next available rate with a low following rate. | +| `2023-01-01T01:00` | `2023-01-02T00:00` - `2023-01-02T01:00` | `false` | Our lowest period is in the past, so we have to wait until our target period has passed to look at the next evaluation period. | +| `2023-01-01T01:00` | `2023-01-01T04:30` - `2023-01-01T05:30` | `true` | The rate of 6 is in the past, so 7 is our next lowest rate. 12 is smaller rate than 20 so we start in the rate period before to fill our desired hour. | +| `2023-01-01T23:30` | None | `true` | There is no longer enough time available in the current 24 hour period, so we have to wait until our target period has passed to look at the next evaluation period. | + +If we set our from/to times for `05:00` to `19:00`, we then limit the period that we look at. The following table shows what this would be like. + +| current date/time | period | `Re-evaluate multiple times a day` | reasoning | +| ------------------ | ------------------------------------- | ---------------------------------- | --------- | +| `2023-01-01T00:00` | `2023-01-01T05:00` - `2023-01-01T06:00` | `false` | The rate of 12 is no longer available as it's outside of our `from` time. | +| `2023-01-01T06:30` | `2023-01-02T05:00` - `2023-01-02T06:00` | `false` | Our lowest period is in the past, so we have to wait until our target period has passed to look at the next evaluation period. | +| `2023-01-01T06:30` | `2023-01-01T06:30` - `2023-01-01T07:30` | `true` | The rate of 7 is in the past, so we must look for the next lowest combined rate | +| `2023-01-01T18:00` | `2023-01-01T18:00` - `2023-01-01T19:00` | `true` | The rate of 20 is in the past, so we must look for the next lowest combined rate which is 34 | +| `2023-01-01T18:30` | None | `true` | There is no longer enough time available within our restricted time, so we have to wait until our target period has passed to look at the next evaluation period. | + +If we set our from/to times to look over two days, from `20:00` to `06:00`, we then limit the period that we look at to overnight. The following table shows what this would be like. + +| current date/time | period | `Re-evaluate multiple times a day` | reasoning | +| ------------------ | ------------------------------------- | ---------------------------------- | --------- | +| `2023-01-01T20:00` | `2023-01-01T23:30` - `2023-01-02T01:30` | `false` | Our lowest rate of 5 now falls between our overnight time period so is available | +| `2023-01-02T02:00` | `2023-01-01T23:30` - `2023-01-02T01:30` | `false` | Our lowest period is in the past, so we have to wait until our target period has passed to look at the next evaluation period. | +| `2023-01-02T02:00` | `2023-01-02T04:30` - `2023-01-02T05:30` | `true` | The rate of 5 is in the past, so we must look for the next lowest combined rate, which includes our half hour rate at 7 | +| `2023-01-02T05:30` | None | `true` | There is no longer enough time available within our restricted time, so we have to wait until our target period has passed to look at the next evaluation period. | + +If we set an offset of `-00:30:00`, then while the times might be the same, the target rate sensor will turn on 30 minutes before the select rate period starts. Any set time restrictions **will** include the offset. -This feature is toggled on/off by the `Re-evaluate multiple times a day` checkbox. +##### Intermittent + +If we look at an intermittent sensor that we want on for 1 hour total (but not necessarily together). + +If we set no from/to times, then our 24 hour period being looked at ranges from `00:00:00` to `23:59:59`. + +The following table shows what this would be like. + +| current date/time | period | `Re-evaluate multiple times a day` | reasoning | +| ------------------ | ------------------------------------- | ---------------------------------- | --------- | +| `2023-01-01T00:00` | `2023-01-01T00:00` - `2023-01-01T00:30`, `2023-01-01T23:30` - `2023-01-02T00:00` | `false` | Our sensor will go on for 30 minutes at the lowest intensity, then 30 minutes at the next lowest intensity. | +| `2023-01-01T01:00` | `2023-01-01T00:00` - `2023-01-01T00:30`, `2023-01-01T23:30` - `2023-01-02T00:00` | `false` | Our sensor will go on for 30 minutes at the lowest intensity, which will be in the past, then 30 minutes at the next lowest intensity. | +| `2023-01-01T01:00` | `2023-01-01T05:00` - `2023-01-01T05:30`, `2023-01-01T23:30` - `2023-01-02T00:00` | `true` | Our sensor will go on for 30 minutes at the second lowest intensity, then 30 minutes at the third lowest intensity. | +| `2023-01-01T23:30` | None | `true` | There is no longer enough time available in the current 24 hour period, so we have to wait until our target period has passed to look at the next evaluation period. | + +If we set our from/to times for `05:00` to `19:00`, we then limit the period that we look at. The following table shows what this would be like. + +| current date/time | period | `Re-evaluate multiple times a day` | reasoning | +| ------------------ | ------------------------------------- | ---------------------------------- | --------- | +| `2023-01-01T00:00` | `2023-01-01T05:00` - `2023-01-01T05:30`, `2023-01-01T05:30` - `2023-01-01T06:00` | `false` | Our lowest intensities are outside our target range, so we need to look at the next cheapest. Luckily on our scenario the two lowest intensities are next to each other. | +| `2023-01-01T06:30` | `2023-01-01T05:00` - `2023-01-01T05:30`, `2023-01-01T05:30` - `2023-01-01T06:00` | `false` | Both of our lowest intensities in the target range are in the past. | +| `2023-01-01T06:30` | `2023-01-01T06:30` - `2023-01-01T07:00`, `2023-01-01T07:00` - `2023-01-01T07:30` | `true` | Both of our lowest intensities in the target range are in the past, so we must look for the next lowest combined rate | +| `2023-01-01T18:30` | None | `true` | There is no longer enough time available within our restricted time, so we have to wait until our target period has passed to look at the next evaluation period. | + +If we set our from/to times to look over two days, from `20:00` to `06:00`, we then limit the period that we look at to overnight. The following table shows what this would be like. + +| current date/time | period | `Re-evaluate multiple times a day` | reasoning | +| ------------------ | ------------------------------------- | ---------------------------------- | --------- | +| `2023-01-01T20:00` | `2023-01-01T23:30` - `2023-01-02T00:30`, `2023-01-02T05:00` - `2023-01-02T05:30` | `false` | Our lowest rate of 5 now falls between our overnight time period so is available | +| `2023-01-02T02:00` | `2023-01-01T23:30` - `2023-01-02T00:30`, `2023-01-02T05:00` - `2023-01-02T05:30` | `false` | Our lowest period is in the past, but we still have a rate in the future so our sensor will only come on once. | +| `2023-01-02T02:00` | `2023-01-02T02:00` - `2023-01-02T02:30`, `2023-01-02T05:00` - `2023-01-02T05:30` | `true` | The rate of 5 is in the past, so we must look for the next lowest combined rate, which includes our half hour rate at 7 | +| `2023-01-02T05:30` | None | `true` | There is no longer enough time available within our restricted time, so we have to wait until our target period has passed to look at the next evaluation period. | + +If we set an offset of `-00:30:00`, then while the times might be the same, the target rate sensor will turn on 30 minutes before the select rate period starts. Any set time restrictions **will** include the offset. + +## Services + +### Service carbon_intensity.update_target_config + +Service for updating a given target rate's config. This allows you to change target rates sensors dynamically based on other outside criteria (e.g. you need to adjust the target hours to top up home batteries). + +> Please note this is temporary and will not persist between restarts. + +| Attribute | Optional | Description | +| ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------| +| `target.entity_id` | `no` | The name of the target sensor whose configuration is to be updated | +| `data.hours` | `yes` | The optional number of hours the rate sensor should come on during a 24 hour period. Must be divisible by 0.5. | +| `data.start_time` | `yes` | The optional time the evaluation period should start. Must be in the format of `HH:MM`. | +| `data.end_time` | `yes` | The optional time the evaluation period should end. Must be in the format of `HH:MM`. | +| `data.offset` | `yes` | The optional offset to apply to the target rate when it starts. Must be in the format `(+/-)HH:MM:SS` | + +This can be used via automations in the following way. Assuming we have the following inputs. + +```yaml +input_number: + carbon_intensity_hours: + name: Carbon Intensity Target Hours + min: 0 + max: 24 + +input_text: + # From/to would ideally use input_datetime, but we need the time in a different format + carbon_intensity_from: + name: Carbon Intensity Target From + initial: "00:00" + carbon_intensity_to: + name: Carbon Intensity Target To + initial: "00:00" + carbon_intensity_offset: + name: Carbon Intensity Target Offset + initial: "-00:00:00" +``` + +Then an automation might look like the following + +```yaml +automations: + - alias: Update target rate config + trigger: + - platform: state + entity_id: + - input_number.carbon_intensity_hours + - input_text.carbon_intensity_from + - input_text.carbon_intensity_to + - input_text.carbon_intensity_offset + condition: [] + action: + - service: carbon_intensity.update_target_config + data: + hours: > + "{{ states('input_number.carbon_intensity_hours') | string }}" + start_time: > + {{ states('input_text.carbon_intensity_from') }} + end_time: > + {{ states('input_text.carbon_intensity_to') }} + offset: > + {{ states('input_text.carbon_intensity_offset') }} + target: + entity_id: binary_sensor.carbon_intensity_target_example +``` ## FAQ diff --git a/custom_components/carbon_intensity/binary_sensors/target_rate.py b/custom_components/carbon_intensity/binary_sensors/target_rate.py index ad516ae..cb5f653 100644 --- a/custom_components/carbon_intensity/binary_sensors/target_rate.py +++ b/custom_components/carbon_intensity/binary_sensors/target_rate.py @@ -1,5 +1,8 @@ import logging +import voluptuous as vol + +from homeassistant.core import callback from homeassistant.util.dt import (utcnow, now) from homeassistant.helpers.update_coordinator import ( CoordinatorEntity @@ -16,6 +19,9 @@ CONFIG_TARGET_START_TIME, CONFIG_TARGET_END_TIME, CONFIG_TARGET_ROLLING_TARGET, + REGEX_HOURS, + REGEX_OFFSET_PARTS, + REGEX_TIME, ) from ..utils import apply_offset @@ -145,4 +151,63 @@ async def async_added_to_hass(self): for x in state.attributes.keys(): self._attributes[x] = state.attributes[x] - _LOGGER.debug(f'Restored CarbonIntensityTargetRate state: {self._state}') \ No newline at end of file + _LOGGER.debug(f'Restored CarbonIntensityTargetRate state: {self._state}') + + @callback + def async_update_config(self, target_start_time=None, target_end_time=None, target_hours=None, target_offset=None): + """Update sensors config""" + + config = dict(self._config) + + if target_hours is not None: + # Inputs from automations can include quotes, so remove these + trimmed_target_hours = target_hours.strip('\"') + matches = re.search(REGEX_HOURS, trimmed_target_hours) + if matches == None: + raise vol.Invalid(f"Target hours of '{trimmed_target_hours}' must be in half hour increments.") + else: + trimmed_target_hours = float(trimmed_target_hours) + if trimmed_target_hours % 0.5 != 0: + raise vol.Invalid(f"Target hours of '{trimmed_target_hours}' must be in half hour increments.") + else: + config.update({ + CONFIG_TARGET_HOURS: trimmed_target_hours + }) + + if target_start_time is not None: + # Inputs from automations can include quotes, so remove these + trimmed_target_start_time = target_start_time.strip('\"') + matches = re.search(REGEX_TIME, trimmed_target_start_time) + if matches == None: + raise vol.Invalid("Start time must be in the format HH:MM") + else: + config.update({ + CONFIG_TARGET_START_TIME: trimmed_target_start_time + }) + + if target_end_time is not None: + # Inputs from automations can include quotes, so remove these + trimmed_target_end_time = target_end_time.strip('\"') + matches = re.search(REGEX_TIME, trimmed_target_end_time) + if matches == None: + raise vol.Invalid("End time must be in the format HH:MM") + else: + config.update({ + CONFIG_TARGET_END_TIME: trimmed_target_end_time + }) + + if target_offset is not None: + # Inputs from automations can include quotes, so remove these + trimmed_target_offset = target_offset.strip('\"') + matches = re.search(REGEX_OFFSET_PARTS, trimmed_target_offset) + if matches == None: + raise vol.Invalid("Offset must be in the form of HH:MM:SS with an optional negative symbol") + else: + config.update({ + CONFIG_TARGET_OFFSET: trimmed_target_offset + }) + + self._config = config + self._attributes = self._config.copy() + self._target_rates = [] + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/carbon_intensity/services.yaml b/custom_components/carbon_intensity/services.yaml new file mode 100644 index 0000000..c98d727 --- /dev/null +++ b/custom_components/carbon_intensity/services.yaml @@ -0,0 +1,32 @@ +update_target_config: + name: Update target rate config + description: Updates a given target rate's config. Please note this is temporary and will not persist between restarts. + target: + entity: + integration: carbon_intensity + domain: binary_sensor + fields: + hours: + name: Hours + description: The optional number of hours the target rate sensor should come on during a 24 hour period. + example: '1.5' + selector: + text: + start_time: + name: Start time + description: The optional time the evaluation period should start. + example: '06:00' + selector: + text: + end_time: + name: End time + description: The optional time the evaluation period should end. + example: '19:00' + selector: + text: + offset: + name: Offset + description: + The optional offset to apply to the target rate when it starts + selector: + text: \ No newline at end of file