From 0ace45e325bdbb104a24787bc26556e09e3d804e Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Thu, 19 Dec 2024 19:20:37 +0000 Subject: [PATCH 01/12] feat: Updated Home Pro to contact local API directly instead of via custom API (45 minutes dev time) --- .../api_client_home_pro/__init__.py | 35 +++++++++++-------- .../octopus_energy/config/main.py | 4 +++ custom_components/octopus_energy/const.py | 2 +- tests/integration/__init__.py | 3 ++ tests/local_integration/__init__.py | 3 ++ .../test_async_get_consumption.py | 21 ----------- .../api_client_home_pro/test_async_ping.py | 17 --------- 7 files changed, 31 insertions(+), 54 deletions(-) diff --git a/custom_components/octopus_energy/api_client_home_pro/__init__.py b/custom_components/octopus_energy/api_client_home_pro/__init__.py index de3c31b0..813fb42c 100644 --- a/custom_components/octopus_energy/api_client_home_pro/__init__.py +++ b/custom_components/octopus_energy/api_client_home_pro/__init__.py @@ -42,9 +42,10 @@ def _create_client_session(self): async def async_ping(self): try: client = self._create_client_session() - url = f'{self._base_url}/get_meter_info?meter_type=elec' + url = f'{self._base_url}:3000/get_meter_consumption' headers = { "Authorization": self._api_key } - async with client.get(url, headers=headers) as response: + data = { "meter_type": "elec" } + async with client.post(url, headers=headers, json=data) as response: response_body = await self.__async_read_response__(response, url) if (response_body is not None and "Status" in response_body): status: str = response_body["Status"] @@ -62,20 +63,23 @@ async def async_get_consumption(self, is_electricity: bool) -> list | None: try: client = self._create_client_session() meter_type = 'elec' if is_electricity else 'gas' - url = f'{self._base_url}/get_meter_consumption?meter_type={meter_type}' + url = f'{self._base_url}:3000/get_meter_consumption' headers = { "Authorization": self._api_key } - async with client.get(url, headers=headers) as response: + data = { "meter_type": meter_type } + async with client.post(url, headers=headers, json=data) as response: response_body = await self.__async_read_response__(response, url) - if (response_body is not None and "meter_consump" in response_body and "consum" in response_body["meter_consump"]): - data = response_body["meter_consump"]["consum"] - divisor = int(data["raw"]["divisor"], 16) - return [{ - "total_consumption": int(data["consumption"]) / divisor if divisor > 0 else None, - "demand": float(data["instdmand"]) if "instdmand" in data else None, - "start": datetime.fromtimestamp(int(response_body["meter_consump"]["time"]), timezone.utc), - "end": datetime.fromtimestamp(int(response_body["meter_consump"]["time"]), timezone.utc), - "is_kwh": data["unit"] == 0 - }] + if (response_body is not None and "meter_consump"): + meter_consump = json.loads(response_body["meter_consump"]) + if "consum" in meter_consump: + data = meter_consump["consum"] + divisor = int(data["raw"]["divisor"], 16) + return [{ + "total_consumption": int(data["consumption"]) / divisor if divisor > 0 else None, + "demand": float(data["instdmand"]) if "instdmand" in data else None, + "start": datetime.fromtimestamp(int(meter_consump["time"]), timezone.utc), + "end": datetime.fromtimestamp(int(meter_consump["time"]), timezone.utc), + "is_kwh": data["unit"] == 0 + }] return None @@ -88,7 +92,7 @@ async def async_set_screen(self, value: str, animation_type: str, type: str, bri try: client = self._create_client_session() - url = f'{self._base_url}/screen' + url = f'{self._base_url}:8000/screen' headers = { "Authorization": self._api_key } payload = { # API doesn't support none or empty string as a valid value @@ -110,6 +114,7 @@ async def __async_read_response__(self, response, url): """Reads the response, logging any json errors""" text = await response.text() + _LOGGER.debug(f"response: {text}") if response.status >= 400: if response.status >= 500: diff --git a/custom_components/octopus_energy/config/main.py b/custom_components/octopus_energy/config/main.py index 6a7fc50e..51bd917b 100644 --- a/custom_components/octopus_energy/config/main.py +++ b/custom_components/octopus_energy/config/main.py @@ -37,6 +37,10 @@ async def async_migrate_main_config(version: int, data: {}): new_data[CONFIG_ACCOUNT_ID] = new_data[CONFIG_MAIN_OLD_ACCOUNT_ID] del new_data[CONFIG_MAIN_OLD_ACCOUNT_ID] + if (version <= 5): + if CONFIG_MAIN_HOME_PRO_ADDRESS in new_data: + new_data[CONFIG_MAIN_HOME_PRO_ADDRESS] = f"{new_data[CONFIG_MAIN_HOME_PRO_ADDRESS]}".replace(":8000", "") + return new_data def merge_main_config(data: dict, options: dict, updated_config: dict = None): diff --git a/custom_components/octopus_energy/const.py b/custom_components/octopus_energy/const.py index aefbd324..3b76e269 100644 --- a/custom_components/octopus_energy/const.py +++ b/custom_components/octopus_energy/const.py @@ -17,7 +17,7 @@ REFRESH_RATE_IN_MINUTES_GREENNESS_FORECAST = 180 REFRESH_RATE_IN_MINUTES_HOME_PRO_CONSUMPTION = 0.17 -CONFIG_VERSION = 4 +CONFIG_VERSION = 5 CONFIG_KIND = "kind" CONFIG_KIND_ACCOUNT = "account" diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 917288cd..40cfc6bb 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -1,6 +1,9 @@ +import logging import os from datetime import datetime, timedelta +logging.getLogger().setLevel(logging.DEBUG) + class TestContext: api_key: str account_id: str diff --git a/tests/local_integration/__init__.py b/tests/local_integration/__init__.py index 522cc8ef..89e2a852 100644 --- a/tests/local_integration/__init__.py +++ b/tests/local_integration/__init__.py @@ -1,5 +1,8 @@ +import logging import os +logging.getLogger().setLevel(logging.DEBUG) + class TestContext: def __init__(self, api_key: str, base_url: str): self.api_key = api_key diff --git a/tests/local_integration/api_client_home_pro/test_async_get_consumption.py b/tests/local_integration/api_client_home_pro/test_async_get_consumption.py index 45e52ceb..3baab51b 100644 --- a/tests/local_integration/api_client_home_pro/test_async_get_consumption.py +++ b/tests/local_integration/api_client_home_pro/test_async_get_consumption.py @@ -4,27 +4,6 @@ from custom_components.octopus_energy.api_client import AuthenticationException from custom_components.octopus_energy.api_client_home_pro import OctopusEnergyHomeProApiClient -@pytest.mark.asyncio -@pytest.mark.parametrize("is_electricity",[ - (True), - (False) -]) -async def test_when_get_consumption_is_called_and_api_key_is_invalid_then_exception_is_raised(is_electricity: bool): - # Arrange - context = get_test_context() - - client = OctopusEnergyHomeProApiClient(context.base_url, "invalid-api-key") - - # Act - exception_raised = False - try: - await client.async_get_consumption(is_electricity) - except AuthenticationException: - exception_raised = True - - # Assert - assert exception_raised == True - @pytest.mark.asyncio @pytest.mark.parametrize("is_electricity",[ (True), diff --git a/tests/local_integration/api_client_home_pro/test_async_ping.py b/tests/local_integration/api_client_home_pro/test_async_ping.py index 8467eac1..e212d97b 100644 --- a/tests/local_integration/api_client_home_pro/test_async_ping.py +++ b/tests/local_integration/api_client_home_pro/test_async_ping.py @@ -4,23 +4,6 @@ from custom_components.octopus_energy.api_client import AuthenticationException from custom_components.octopus_energy.api_client_home_pro import OctopusEnergyHomeProApiClient -@pytest.mark.asyncio -async def test_when_ping_is_called_and_api_key_is_invalid_then_exception_is_raised(): - # Arrange - context = get_test_context() - - client = OctopusEnergyHomeProApiClient(context.base_url, "invalid-api-key") - - # Act - exception_raised = False - try: - await client.async_ping() - except AuthenticationException: - exception_raised = True - - # Assert - assert exception_raised == True - @pytest.mark.asyncio async def test_when_ping_is_called_then_data_is_returned(): # Arrange From 8b94c7d15109476acb53f930762c7ee17a4e0ed6 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Sat, 21 Dec 2024 09:53:13 +0000 Subject: [PATCH 02/12] feat: Updated Home Pro config to support custom API being optional if certain features are not required (see docs for more information) (45 minutes dev time) --- _docs/entities/home_pro.md | 6 +++- _docs/setup/account.md | 11 +++++-- custom_components/octopus_energy/__init__.py | 6 ++-- .../api_client_home_pro/__init__.py | 15 +++++----- .../octopus_energy/config/main.py | 8 ++--- custom_components/octopus_energy/text.py | 5 ++-- .../octopus_energy/translations/en.json | 12 ++++---- .../unit/config/test_validate_main_config.py | 30 ------------------- 8 files changed, 35 insertions(+), 58 deletions(-) diff --git a/_docs/entities/home_pro.md b/_docs/entities/home_pro.md index 42161d86..03d38cb2 100644 --- a/_docs/entities/home_pro.md +++ b/_docs/entities/home_pro.md @@ -13,4 +13,8 @@ Once configured, the following entities will retrieve data locally from your Oct `text.octopus_energy_{{ACCOUNT_ID}}_home_pro_screen` -Allows you to set scrolling text for the home pro device. If the text is greater than 3 characters, then it will scroll on the device, otherwise it will be statically displayed. \ No newline at end of file +!!! info + + This is only available if you have setup the [Custom API](../setup/account.md#home-pro). + +Allows you to set scrolling text on the home pro device. If the text is greater than 3 characters, then it will scroll on the device, otherwise it will be statically displayed. \ No newline at end of file diff --git a/_docs/setup/account.md b/_docs/setup/account.md index 09e1ed05..2341ff58 100644 --- a/_docs/setup/account.md +++ b/_docs/setup/account.md @@ -57,7 +57,13 @@ If you are lucky enough to own an [Octopus Home Pro](https://forum.octopus.energ ### Prerequisites -The Octopus Home Pro has an internal API which is not currently exposed. In order to make this data available for consumption by this integration you will need to expose a custom API on your device by following the instructions below +The Octopus Home Pro has a local API which is used to get consumption and demand data. If this is all you need, then you can jump straight to the [settings](./account.md#settings). + +However, there is also an internal API for setting the display which is not currently exposed. In order to make this available for consumption by this integration you will need to expose a custom API on your device by following the instructions below + +!!! warning + + This custom API can only be configured with the default Home Pro setup. If you set up Home Assistant on your Home Pro device, then it won't be possible to expose this custom API. 1. Follow [the instructions](https://github.com/OctopusSmartEnergy/Home-Pro-SDK-Public/blob/main/Home.md#sdk) to connect to your Octopus Home Pro via SSH 2. Run the command `wget -O setup_ha.sh https://raw.githubusercontent.com/BottlecapDave/HomeAssistant-OctopusEnergy/main/home_pro_server/setup.sh` to download the installation script @@ -90,8 +96,9 @@ export SERVER_AUTH_TOKEN=thisisasecrettoken # Replace with your own unique strin ### Settings -Once the API has been configured, you will need to set the address to the IP address of your Octopus Home Pro followed by the port 8000 (e.g. `http://192.168.1.2:8000`) and the api key to the value you set `SERVER_AUTH_TOKEN` to. +Once the API has been configured, you will need to set the address to the IP address of your Octopus Home Pro (e.g. `http://192.168.1.2`). +If you have setup the custom API, then you will need to set api key to the value you set `SERVER_AUTH_TOKEN` to. ### Entities diff --git a/custom_components/octopus_energy/__init__.py b/custom_components/octopus_energy/__init__.py index 28649fe9..5e17c70a 100644 --- a/custom_components/octopus_energy/__init__.py +++ b/custom_components/octopus_energy/__init__.py @@ -283,10 +283,8 @@ async def async_setup_dependencies(hass, config): hass.data[DOMAIN][account_id][DATA_CLIENT] = client if (CONFIG_MAIN_HOME_PRO_ADDRESS in config and - config[CONFIG_MAIN_HOME_PRO_ADDRESS] is not None and - CONFIG_MAIN_HOME_PRO_API_KEY in config and - config[CONFIG_MAIN_HOME_PRO_API_KEY] is not None): - home_pro_client = OctopusEnergyHomeProApiClient(config[CONFIG_MAIN_HOME_PRO_ADDRESS], config[CONFIG_MAIN_HOME_PRO_API_KEY]) + config[CONFIG_MAIN_HOME_PRO_ADDRESS] is not None): + home_pro_client = OctopusEnergyHomeProApiClient(config[CONFIG_MAIN_HOME_PRO_ADDRESS], config[CONFIG_MAIN_HOME_PRO_API_KEY] if CONFIG_MAIN_HOME_PRO_API_KEY in config else None) hass.data[DOMAIN][account_id][DATA_HOME_PRO_CLIENT] = home_pro_client # Delete any issues that may have been previously raised diff --git a/custom_components/octopus_energy/api_client_home_pro/__init__.py b/custom_components/octopus_energy/api_client_home_pro/__init__.py index 813fb42c..df5e11d9 100644 --- a/custom_components/octopus_energy/api_client_home_pro/__init__.py +++ b/custom_components/octopus_energy/api_client_home_pro/__init__.py @@ -12,9 +12,6 @@ class OctopusEnergyHomeProApiClient: _session_lock = RLock() def __init__(self, base_url: str, api_key: str, timeout_in_seconds = 20): - if (api_key is None): - raise Exception('API KEY is not set') - if (base_url is None): raise Exception('BaseUrl is not set') @@ -26,6 +23,9 @@ def __init__(self, base_url: str, api_key: str, timeout_in_seconds = 20): self._session = None + def has_api_key(self): + return self._api_key is not None + async def async_close(self): with self._session_lock: if self._session is not None: @@ -43,9 +43,8 @@ async def async_ping(self): try: client = self._create_client_session() url = f'{self._base_url}:3000/get_meter_consumption' - headers = { "Authorization": self._api_key } data = { "meter_type": "elec" } - async with client.post(url, headers=headers, json=data) as response: + async with client.post(url, json=data) as response: response_body = await self.__async_read_response__(response, url) if (response_body is not None and "Status" in response_body): status: str = response_body["Status"] @@ -64,9 +63,8 @@ async def async_get_consumption(self, is_electricity: bool) -> list | None: client = self._create_client_session() meter_type = 'elec' if is_electricity else 'gas' url = f'{self._base_url}:3000/get_meter_consumption' - headers = { "Authorization": self._api_key } data = { "meter_type": meter_type } - async with client.post(url, headers=headers, json=data) as response: + async with client.post(url, json=data) as response: response_body = await self.__async_read_response__(response, url) if (response_body is not None and "meter_consump"): meter_consump = json.loads(response_body["meter_consump"]) @@ -90,6 +88,9 @@ async def async_get_consumption(self, is_electricity: bool) -> list | None: async def async_set_screen(self, value: str, animation_type: str, type: str, brightness: int, animation_interval: int): """Get the latest consumption""" + if self._api_key is None: + raise Exception('API key is not set, so screen cannot be contacted') + try: client = self._create_client_session() url = f'{self._base_url}:8000/screen' diff --git a/custom_components/octopus_energy/config/main.py b/custom_components/octopus_energy/config/main.py index 51bd917b..78013a54 100644 --- a/custom_components/octopus_energy/config/main.py +++ b/custom_components/octopus_energy/config/main.py @@ -98,11 +98,7 @@ async def async_validate_main_config(data, account_ids = []): if data[CONFIG_MAIN_LIVE_GAS_CONSUMPTION_REFRESH_IN_MINUTES] < 1: errors[CONFIG_MAIN_LIVE_GAS_CONSUMPTION_REFRESH_IN_MINUTES] = "value_greater_than_zero" - if ((CONFIG_MAIN_HOME_PRO_ADDRESS in data and - data[CONFIG_MAIN_HOME_PRO_ADDRESS] is not None and - (CONFIG_MAIN_HOME_PRO_API_KEY not in data or data[CONFIG_MAIN_HOME_PRO_API_KEY] is None)) or - - (CONFIG_MAIN_HOME_PRO_API_KEY in data and + if ((CONFIG_MAIN_HOME_PRO_API_KEY in data and data[CONFIG_MAIN_HOME_PRO_API_KEY] is not None and (CONFIG_MAIN_HOME_PRO_ADDRESS not in data or data[CONFIG_MAIN_HOME_PRO_ADDRESS] is None))): errors[CONFIG_MAIN_HOME_PRO_ADDRESS] = "all_home_pro_values_not_set" @@ -111,7 +107,7 @@ async def async_validate_main_config(data, account_ids = []): data[CONFIG_MAIN_HOME_PRO_ADDRESS] is not None and CONFIG_MAIN_HOME_PRO_API_KEY in data and data[CONFIG_MAIN_HOME_PRO_API_KEY] is not None): - home_pro_client = OctopusEnergyHomeProApiClient(data[CONFIG_MAIN_HOME_PRO_ADDRESS], data[CONFIG_MAIN_HOME_PRO_API_KEY]) + home_pro_client = OctopusEnergyHomeProApiClient(data[CONFIG_MAIN_HOME_PRO_ADDRESS], data[CONFIG_MAIN_HOME_PRO_API_KEY] if CONFIG_MAIN_HOME_PRO_API_KEY in data else None) try: can_connect = await home_pro_client.async_ping() diff --git a/custom_components/octopus_energy/text.py b/custom_components/octopus_energy/text.py index c16f28b6..90834254 100644 --- a/custom_components/octopus_energy/text.py +++ b/custom_components/octopus_energy/text.py @@ -3,6 +3,7 @@ from homeassistant.core import HomeAssistant from .home_pro.screen_text import OctopusEnergyHomeProScreenText +from .api_client_home_pro import OctopusEnergyHomeProApiClient from .const import ( CONFIG_ACCOUNT_ID, @@ -27,11 +28,11 @@ async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_default_sensors(hass: HomeAssistant, config, async_add_entities): account_id = config[CONFIG_ACCOUNT_ID] - home_pro_client = hass.data[DOMAIN][account_id][DATA_HOME_PRO_CLIENT] if DATA_HOME_PRO_CLIENT in hass.data[DOMAIN][account_id] else None + home_pro_client: OctopusEnergyHomeProApiClient = hass.data[DOMAIN][account_id][DATA_HOME_PRO_CLIENT] if DATA_HOME_PRO_CLIENT in hass.data[DOMAIN][account_id] else None entities = [] - if home_pro_client is not None: + if home_pro_client is not None and home_pro_client.has_api_key(): entities.append(OctopusEnergyHomeProScreenText(hass, account_id, home_pro_client)) async_add_entities(entities) \ No newline at end of file diff --git a/custom_components/octopus_energy/translations/en.json b/custom_components/octopus_energy/translations/en.json index bce3a406..0475f6e9 100644 --- a/custom_components/octopus_energy/translations/en.json +++ b/custom_components/octopus_energy/translations/en.json @@ -13,7 +13,7 @@ "calorific_value": "Gas calorific value.", "electricity_price_cap": "Optional electricity price cap in pence", "gas_price_cap": "Optional gas price cap in pence", - "home_pro_address": "Home Pro address and port (e.g. http://localhost:8000)", + "home_pro_address": "Home Pro address (e.g. http://localhost)", "home_pro_api_key": "Home Pro API key", "favour_direct_debit_rates": "Favour direct debit rates where available" }, @@ -24,7 +24,7 @@ "electricity_price_cap": "This usually comes from the OE APIs and doesn't need to be set", "gas_price_cap": "This usually comes from the OE APIs and doesn't need to be set", "home_pro_address": "WARNING: This is experimental.", - "home_pro_api_key": "WARNING: This is experimental" + "home_pro_api_key": "WARNING: This is experimental. This is only required if you have setup the custom API." } }, "target_rate": { @@ -137,7 +137,7 @@ "weighting_not_supported_for_hour_mode": "Weighting is not supported for this hour mode", "invalid_product_or_tariff": "Product or tariff code does not exist", "minimum_or_maximum_rate_not_specified": "Either minimum and/or maximum rate must be specified for minimum hours mode", - "all_home_pro_values_not_set": "Either both Home Pro address and API key must be set, or neither must be set", + "all_home_pro_values_not_set": "Home Pro address must be set if API key is set", "home_pro_connection_failed": "Cannot connect to Home Pro device. Please check the specified address is correct and that you've installed the custom API as per the instructions.", "home_pro_authentication_failed": "Cannot authenticate with API on Home Pro device. Please check authentication token matches the value you configured.", "home_pro_not_responding": "Connected to Home Pro device, but responding with unsuccessful status. This implies the Home Pro failed to connect to your meter." @@ -160,7 +160,7 @@ "calorific_value": "Gas calorific value", "electricity_price_cap": "Optional electricity price cap in pence", "gas_price_cap": "Optional gas price cap in pence", - "home_pro_address": "Home Pro address and port (e.g. http://localhost:8000)", + "home_pro_address": "Home Pro address (e.g. http://localhost:8000)", "home_pro_api_key": "Home Pro API key", "favour_direct_debit_rates": "Favour direct debit rates where available" }, @@ -170,7 +170,7 @@ "electricity_price_cap": "This usually comes from the OE APIs and doesn't need to be set", "gas_price_cap": "This usually comes from the OE APIs and doesn't need to be set", "home_pro_address": "WARNING: This is experimental", - "home_pro_api_key": "WARNING: This is experimental" + "home_pro_api_key": "WARNING: This is experimental. This is only required if you have setup the custom API." } }, "target_rate": { @@ -256,7 +256,7 @@ "weighting_not_supported_for_hour_mode": "Weighting is not supported for this hour mode", "invalid_product_or_tariff": "Product or tariff code does not exist", "minimum_or_maximum_rate_not_specified": "Either minimum and/or maximum rate must be specified for minimum hours mode", - "all_home_pro_values_not_set": "Either both Home Pro address and API key must be set, or neither must be set", + "all_home_pro_values_not_set": "Home Pro address must be set if API key is set", "home_pro_connection_failed": "Cannot connect to Home Pro device. Please check the specified address is correct and that you've installed the custom API as per the instructions.", "home_pro_authentication_failed": "Cannot authenticate with API on Home Pro device. Please check authentication token matches the value you configured.", "home_pro_not_responding": "Connected to Home Pro device, but responding with unsuccessful status. This implies the Home Pro failed to connect to your meter." diff --git a/tests/unit/config/test_validate_main_config.py b/tests/unit/config/test_validate_main_config.py index 646c4d09..ed885a5e 100644 --- a/tests/unit/config/test_validate_main_config.py +++ b/tests/unit/config/test_validate_main_config.py @@ -292,36 +292,6 @@ async def async_mocked_get_account(*args, **kwargs): assert_errors_not_present(errors, config_keys, CONFIG_ACCOUNT_ID) -@pytest.mark.asyncio -async def test_when_home_pro_address_is_set_and_home_pro_api_key_is_not_set_then_error_returned(): - # Arrange - data = { - CONFIG_MAIN_API_KEY: "test-api-key", - CONFIG_ACCOUNT_ID: "A-123", - CONFIG_MAIN_SUPPORTS_LIVE_CONSUMPTION: True, - CONFIG_MAIN_LIVE_ELECTRICITY_CONSUMPTION_REFRESH_IN_MINUTES: 1, - CONFIG_MAIN_LIVE_GAS_CONSUMPTION_REFRESH_IN_MINUTES: 1, - CONFIG_MAIN_CALORIFIC_VALUE: 40, - CONFIG_MAIN_ELECTRICITY_PRICE_CAP: 38.5, - CONFIG_MAIN_GAS_PRICE_CAP: 10.5, - CONFIG_MAIN_HOME_PRO_ADDRESS: "http://localhost:8000", - CONFIG_MAIN_HOME_PRO_API_KEY: None - } - - account_info = get_account_info() - async def async_mocked_get_account(*args, **kwargs): - return account_info - - # Act - with mock.patch.multiple(OctopusEnergyApiClient, async_get_account=async_mocked_get_account): - errors = await async_validate_main_config(data) - - # Assert - assert CONFIG_MAIN_HOME_PRO_ADDRESS in errors - assert errors[CONFIG_MAIN_HOME_PRO_ADDRESS] == "all_home_pro_values_not_set" - - assert_errors_not_present(errors, config_keys, CONFIG_MAIN_HOME_PRO_ADDRESS) - @pytest.mark.asyncio async def test_when_home_pro_address_is_not_set_and_home_pro_api_key_is_set_then_error_returned(): # Arrange From 9350c3fbdb46514033a175e1db7e521b2fc07835 Mon Sep 17 00:00:00 2001 From: David Kendall Date: Tue, 24 Dec 2024 20:25:18 +0000 Subject: [PATCH 03/12] feat: Added ability to apply weightings to rates from external sources for use with target rate and rolling target rate sensors (4 hours 30 minutes dev time) --- .github/actions/setup/action.yml | 14 ++ .github/workflows/docs.yml | 4 + .github/workflows/main.yml | 19 +- _docs/services.md | 34 +++- _docs/setup/rolling_target_rate.md | 6 + _docs/setup/target_rate.md | 6 + custom_components/octopus_energy/__init__.py | 7 + custom_components/octopus_energy/const.py | 1 + .../octopus_energy/electricity/base.py | 1 + .../electricity/current_rate.py | 38 +++- .../octopus_energy/manifest.json | 1 + custom_components/octopus_energy/sensor.py | 26 ++- .../octopus_energy/services.yaml | 24 ++- .../octopus_energy/storage/rate_weightings.py | 28 +++ .../target_rates/rolling_target_rate.py | 14 +- .../target_rates/target_rate.py | 15 +- .../octopus_energy/translations/en.json | 3 + .../octopus_energy/utils/weightings.py | 117 +++++++++++ requirements.test.txt | 1 + tests/unit/utils/test_apply_weighting.py | 51 +++++ tests/unit/utils/test_merge_weightings.py | 98 ++++++++++ .../utils/test_validate_rate_weightings.py | 184 ++++++++++++++++++ 22 files changed, 667 insertions(+), 25 deletions(-) create mode 100644 .github/actions/setup/action.yml create mode 100644 custom_components/octopus_energy/storage/rate_weightings.py create mode 100644 custom_components/octopus_energy/utils/weightings.py create mode 100644 tests/unit/utils/test_apply_weighting.py create mode 100644 tests/unit/utils/test_merge_weightings.py create mode 100644 tests/unit/utils/test_validate_rate_weightings.py diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 00000000..62745884 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,14 @@ +name: Setup dependencies +description: Sets up required dependencies +runs: + using: composite + steps: + - name: Install dependencies + run: sudo apt install libffi-dev libncurses5-dev zlib1g zlib1g-dev libssl-dev libreadline-dev libbz2-dev libsqlite3-dev + shell: bash + - name: asdf_install + uses: asdf-vm/actions/install@v3 + - name: Install Python modules + run: | + pip install -r requirements.test.txt + shell: bash \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0a732632..bc537e82 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,6 +6,10 @@ on: - 'mkdocs.yml' - '_docs/**' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f1403915..e0cdc102 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,6 +16,11 @@ on: paths-ignore: - 'mkdocs.yml' - '_docs/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: validate: if: ${{ github.event_name != 'schedule' || github.repository_owner == 'BottlecapDave' }} @@ -37,11 +42,8 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - name: asdf_install - uses: asdf-vm/actions/install@v3 - - name: Install Python modules - run: | - pip install -r requirements.test.txt + - name: Setup + uses: ./.github/actions/setup - name: Run unit tests run: | python -m pytest tests/unit @@ -55,11 +57,8 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - name: asdf_install - uses: asdf-vm/actions/install@v3 - - name: Install Python modules - run: | - pip install -r requirements.test.txt + - name: Setup + uses: ./.github/actions/setup - name: Run integration tests run: | python -m pytest tests/integration diff --git a/_docs/services.md b/_docs/services.md index 25772394..30d27cbf 100644 --- a/_docs/services.md +++ b/_docs/services.md @@ -235,4 +235,36 @@ Allows you to adjust the consumption for any given period recorded by a [cost tr | ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------- | | `target.entity_id` | `no` | The name of the cost tracker sensor(s) that should be updated (e.g. `sensor.octopus_energy_cost_tracker_{{COST_TRACKER_NAME}}`). | | `data.date` | `no` | The date of the data within the cost tracker to be adjusted. | -| `data.consumption` | `no` | The new consumption recorded against the specified date. | \ No newline at end of file +| `data.consumption` | `no` | The new consumption recorded against the specified date. | + +## octopus_energy.register_rate_weightings + +Allows you to configure weightings against rates at given times using factors external to the integration. These are applied when calculating [target rates](./setup/target_rate.md#external-rate-weightings) or [rolling target rates](./setup/rolling_target_rate.md#external-rate-weightings). + +Rate weightings are added to any existing rate weightings that have been previously configured. Any rate weightings that are more than 24 hours old are removed. Any rate weightings for periods that have been previously configured are overridden. + +| Attribute | Optional | Description | +| ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------- | +| `target.entity_id` | `no` | The name of the electricity current rate sensor for the rates the weighting should be applied to (e.g. `sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_current_rate`). | +| `data.weightings` | `no` | The collection of weightings to add. Each item in the array should represent a given 30 minute period. Example array is `[{ "start": "2025-01-01T00:00:00Z", "end": "2025-01-01T00:30:00Z", "weighting": 0.1 }]` | + +### Automation Example + +This automation adds weightings based on the national grids carbon intensity, as provided by [Octopus Energy Carbon Intensity](https://github.com/BottlecapDave/HomeAssistant-CarbonIntensity). + +```yaml +- alias: Carbon Intensity Rate Weightings + triggers: + - platform: state + entity_id: event.carbon_intensity_national_current_day_rates + actions: + - action: octopus_energy.register_rate_weightings + target: + entity_id: sensor.octopus_energy_electricity_xxx_xxx_current_rate + data: + weightings: > + {% set forecast = state_attr('event.carbon_intensity_national_current_day_rates', 'rates') + state_attr('event.carbon_intensity_national_next_day_rates', 'rates') %} + {% set ns = namespace(list = []) %} {%- for a in forecast -%} + {%- set ns.list = ns.list + [{ "start": a.from.strftime('%Y-%m-%dT%H:%M:%SZ'), "end": a.to.strftime('%Y-%m-%dT%H:%M:%SZ'), "weighting": a.intensity_forecast | float }] -%} + {%- endfor -%} {{ ns.list }} +``` \ No newline at end of file diff --git a/_docs/setup/rolling_target_rate.md b/_docs/setup/rolling_target_rate.md index 95056653..f38ccf32 100644 --- a/_docs/setup/rolling_target_rate.md +++ b/_docs/setup/rolling_target_rate.md @@ -136,6 +136,12 @@ If we had a target rate sensor of 1 hour, the following would occur with the fol | 0.2 | `2024-11-26 11:00:00`-`2024-11-26 12:00:00` | Cheapest period would be 0.1p, free electricity period would be 0.02p. | | 0 | `2024-11-26 11:00:00`-`2024-11-26 12:00:00` | Cheapest period would be 0.1p, free electricity period would be 0p. This will always go for free electricity sessions if available. | +## External Rate Weightings + +There may be times when you want to calculate the best times using factors that are external to data available via the integration, like grid carbon intensity or solar forecasts. This is where external rate weightings come in. Using the [Register Rate Weightings service](../services.md#octopus_energyregister_rate_weightings), you can configured weightings against given rates which are then multiplied against the associated rate. For example if you have a weighting of `2` set and a rate of `0.20`, then the rate will be interpreted as `0.40` during calculation. + +These weightings are used in addition to any [weightings](#weighting) configured against the sensor and [free electricity weightings](#free-electricity-weighting). For example if you have rate weight of `2`, a rate of `0.20`, a sensor weight of `3` and free electricity weight of `0.5`, then rate will be interpreted as `0.6` (2 * 0.20 * 3 * 0.5). + ## Attributes The following attributes are available on each sensor diff --git a/_docs/setup/target_rate.md b/_docs/setup/target_rate.md index 37453b1f..d8eb6fec 100644 --- a/_docs/setup/target_rate.md +++ b/_docs/setup/target_rate.md @@ -162,6 +162,12 @@ If we had a target rate sensor of 1 hour, the following would occur with the fol | 0.2 | `2024-11-26 11:00:00`-`2024-11-26 12:00:00` | Cheapest period would be 0.1p, free electricity period would be 0.02p. | | 0 | `2024-11-26 11:00:00`-`2024-11-26 12:00:00` | Cheapest period would be 0.1p, free electricity period would be 0p. This will always go for free electricity sessions if available. | +## External Rate Weightings + +There may be times when you want to calculate the best times using factors that are external to data available via the integration, like grid carbon intensity or solar forecasts. This is where external rate weightings come in. Using the [Register Rate Weightings service](../services.md#octopus_energyregister_rate_weightings), you can configured weightings against given rates which are then multiplied against the associated rate. For example if you have a weighting of `2` set and a rate of `0.20`, then the rate will be interpreted as `0.40` during calculation. + +These weightings are used in addition to any [weightings](#weighting) configured against the sensor and [free electricity weightings](#free-electricity-weighting). For example if you have rate weight of `2`, a rate of `0.20`, a sensor weight of `3` and free electricity weight of `0.5`, then rate will be interpreted as `0.6` (2 * 0.20 * 3 * 0.5). + ## Attributes The following attributes are available on each sensor diff --git a/custom_components/octopus_energy/__init__.py b/custom_components/octopus_energy/__init__.py index 5e17c70a..393fbde6 100644 --- a/custom_components/octopus_energy/__init__.py +++ b/custom_components/octopus_energy/__init__.py @@ -31,6 +31,7 @@ from .utils.error import api_exception_to_string from .storage.account import async_load_cached_account, async_save_cached_account from .storage.intelligent_device import async_load_cached_intelligent_device, async_save_cached_intelligent_device +from .storage.rate_weightings import async_load_cached_rate_weightings from .const import ( CONFIG_FAVOUR_DIRECT_DEBIT_RATES, @@ -44,6 +45,7 @@ CONFIG_MAIN_HOME_PRO_API_KEY, CONFIG_MAIN_OLD_API_KEY, CONFIG_VERSION, + DATA_CUSTOM_RATE_WEIGHTINGS_KEY, DATA_HOME_PRO_CLIENT, DATA_INTELLIGENT_DEVICE, DATA_INTELLIGENT_MPAN, @@ -350,6 +352,11 @@ async def async_setup_dependencies(hass, config): mpan = point["mpan"] electricity_tariff = get_active_tariff(now, point["agreements"]) + rate_weightings = await async_load_cached_rate_weightings(hass, mpan) + if rate_weightings is not None: + key = DATA_CUSTOM_RATE_WEIGHTINGS_KEY.format(mpan) + hass.data[DOMAIN][account_id][key] = rate_weightings + for meter in point["meters"]: serial_number = meter["serial_number"] diff --git a/custom_components/octopus_energy/const.py b/custom_components/octopus_energy/const.py index 1fbd4e88..efbda05e 100644 --- a/custom_components/octopus_energy/const.py +++ b/custom_components/octopus_energy/const.py @@ -139,6 +139,7 @@ DATA_HOME_PRO_CURRENT_CONSUMPTION_KEY = "HOME_PRO_CURRENT_CONSUMPTION_{}" DATA_FREE_ELECTRICITY_SESSIONS = "FREE_ELECTRICITY_SESSIONS" DATA_FREE_ELECTRICITY_SESSIONS_COORDINATOR = "FREE_ELECTRICITY_SESSIONS_COORDINATOR" +DATA_CUSTOM_RATE_WEIGHTINGS_KEY = "DATA_CUSTOM_RATE_WEIGHTINGS_{}" DATA_SAVING_SESSIONS_FORCE_UPDATE = "SAVING_SESSIONS_FORCE_UPDATE" diff --git a/custom_components/octopus_energy/electricity/base.py b/custom_components/octopus_energy/electricity/base.py index 78579521..7252e59b 100644 --- a/custom_components/octopus_energy/electricity/base.py +++ b/custom_components/octopus_energy/electricity/base.py @@ -13,6 +13,7 @@ def __init__(self, hass: HomeAssistant, meter, point, entity_domain = "sensor"): """Init sensor""" self._point = point self._meter = meter + self._hass = hass self._mpan = point["mpan"] self._serial_number = meter["serial_number"] diff --git a/custom_components/octopus_energy/electricity/current_rate.py b/custom_components/octopus_energy/electricity/current_rate.py index 16b706c0..cae19580 100644 --- a/custom_components/octopus_energy/electricity/current_rate.py +++ b/custom_components/octopus_energy/electricity/current_rate.py @@ -1,6 +1,8 @@ -from datetime import timedelta import logging +from custom_components.octopus_energy.storage.rate_weightings import async_save_cached_rate_weightings +from homeassistant.exceptions import ServiceValidationError + from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -20,6 +22,8 @@ from .base import (OctopusEnergyElectricitySensor) from ..utils.attributes import dict_to_typed_dict from ..coordinators.electricity_rates import ElectricityRatesCoordinatorResult +from ..utils.weightings import merge_weightings, validate_rate_weightings +from ..const import DATA_CUSTOM_RATE_WEIGHTINGS_KEY, DOMAIN from ..utils.rate_information import (get_current_rate_information) @@ -28,7 +32,7 @@ class OctopusEnergyElectricityCurrentRate(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the current rate.""" - def __init__(self, hass: HomeAssistant, coordinator, meter, point, electricity_price_cap): + def __init__(self, hass: HomeAssistant, coordinator, meter, point, electricity_price_cap, account_id: str): """Init sensor.""" # Pass coordinator to base class CoordinatorEntity.__init__(self, coordinator) @@ -37,6 +41,7 @@ def __init__(self, hass: HomeAssistant, coordinator, meter, point, electricity_p self._state = None self._last_updated = None self._electricity_price_cap = electricity_price_cap + self._account_id = account_id self._attributes = { "mpan": self._mpan, @@ -155,4 +160,31 @@ async def async_added_to_hass(self): if state is not None and self._state is None: self._state = None if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) else state.state self._attributes = dict_to_typed_dict(state.attributes, ['all_rates', 'applicable_rates']) - _LOGGER.debug(f'Restored OctopusEnergyElectricityCurrentRate state: {self._state}') \ No newline at end of file + _LOGGER.debug(f'Restored OctopusEnergyElectricityCurrentRate state: {self._state}') + + @callback + async def async_register_rate_weightings(self, weightings): + """Apply rate weightings""" + result = validate_rate_weightings(weightings) + if result.success == False: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_rate_weightings", + translation_placeholders={ + "error": result.error_message, + }, + ) + + key = DATA_CUSTOM_RATE_WEIGHTINGS_KEY.format(self._mpan) + weightings = result.weightings + weightings = merge_weightings( + now(), + weightings, + self._hass.data[DOMAIN][self._account_id][key] + if key in self._hass.data[DOMAIN][self._account_id] + else [] + ) + + self._hass.data[DOMAIN][self._account_id][key] = weightings + + await async_save_cached_rate_weightings(self._hass, self._mpan, result.weightings) \ No newline at end of file diff --git a/custom_components/octopus_energy/manifest.json b/custom_components/octopus_energy/manifest.json index 073e7afa..4f58f584 100644 --- a/custom_components/octopus_energy/manifest.json +++ b/custom_components/octopus_energy/manifest.json @@ -13,6 +13,7 @@ "homekit": {}, "iot_class": "cloud_polling", "issue_tracker": "https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/issues", + "requirements": ["pydantic"], "ssdp": [], "version": "13.3.0", "zeroconf": [] diff --git a/custom_components/octopus_energy/sensor.py b/custom_components/octopus_energy/sensor.py index 344465ba..0f516371 100644 --- a/custom_components/octopus_energy/sensor.py +++ b/custom_components/octopus_energy/sensor.py @@ -1,4 +1,4 @@ -from datetime import timedelta +from datetime import datetime, timedelta from custom_components.octopus_energy.api_client.intelligent_device import IntelligentDevice import voluptuous as vol import logging @@ -189,6 +189,28 @@ async def async_setup_entry(hass, entry, async_add_entities): "async_redeem_points_into_account_credit", # supports_response=SupportsResponse.OPTIONAL ) + + platform.async_register_entity_service( + "register_rate_weightings", + vol.All( + cv.make_entity_service_schema( + { + vol.Required("weightings"): vol.All( + cv.ensure_list, + [ + { + vol.Required("start"): str, + vol.Required("end"): str, + vol.Required("weighting"): float + } + ], + ), + }, + extra=vol.ALLOW_EXTRA, + ), + ), + "async_register_rate_weightings", + ) elif config[CONFIG_KIND] == CONFIG_KIND_COST_TRACKER: await async_setup_cost_sensors(hass, entry, config, async_add_entities) @@ -314,7 +336,7 @@ async def async_setup_default_sensors(hass: HomeAssistant, config, async_add_ent electricity_rate_coordinator = hass.data[DOMAIN][account_id][DATA_ELECTRICITY_RATES_COORDINATOR_KEY.format(mpan, serial_number)] electricity_standing_charges_coordinator = await async_setup_electricity_standing_charges_coordinator(hass, account_id, mpan, serial_number) - entities.append(OctopusEnergyElectricityCurrentRate(hass, electricity_rate_coordinator, meter, point, electricity_price_cap)) + entities.append(OctopusEnergyElectricityCurrentRate(hass, electricity_rate_coordinator, meter, point, electricity_price_cap, account_id)) entities.append(OctopusEnergyElectricityPreviousRate(hass, electricity_rate_coordinator, meter, point)) entities.append(OctopusEnergyElectricityNextRate(hass, electricity_rate_coordinator, meter, point)) entities.append(OctopusEnergyElectricityCurrentStandingCharge(hass, electricity_standing_charges_coordinator, meter, point)) diff --git a/custom_components/octopus_energy/services.yaml b/custom_components/octopus_energy/services.yaml index 50a7339d..af826439 100644 --- a/custom_components/octopus_energy/services.yaml +++ b/custom_components/octopus_energy/services.yaml @@ -234,4 +234,26 @@ adjust_cost_tracker: diagnose_heatpump_apis: name: Diagnose heatpump APIs - description: Diagnose available heatpump APIs \ No newline at end of file + description: Diagnose available heatpump APIs + +register_rate_weightings: + name: Register rate weightings + description: Registers external weightings against rates, for use with target rate sensors when calculating target periods. + target: + entity: + integration: octopus_energy + domain: sensor + fields: + weightings: + name: Weightings + description: The collection of time periods and associated weightings to apply. + example: >- + [ + { + "start": "2025-01-01T00:00:00Z", + "end": "2025-01-01T00:30:00Z", + "weighting": 0.1 + } + ] + selector: + object: \ No newline at end of file diff --git a/custom_components/octopus_energy/storage/rate_weightings.py b/custom_components/octopus_energy/storage/rate_weightings.py new file mode 100644 index 00000000..2a7c3fe2 --- /dev/null +++ b/custom_components/octopus_energy/storage/rate_weightings.py @@ -0,0 +1,28 @@ +import logging +from homeassistant.helpers import storage + +from pydantic import BaseModel + +from ..utils.weightings import RateWeighting + +_LOGGER = logging.getLogger(__name__) + +class RateWeightings(BaseModel): + weightings: list[RateWeighting] + +async def async_load_cached_rate_weightings(hass, mpan: str) -> list[RateWeighting]: + store = storage.Store(hass, "1", f"octopus_energy.{mpan}_rate_weightings") + + try: + data = await store.async_load() + if data is not None: + _LOGGER.debug(f"Loaded cached rate weightings for {mpan}") + return RateWeightings.parse_obj(data).weightings + except: + return None + +async def async_save_cached_rate_weightings(hass, mpan: str, weightings: list[RateWeighting]): + if weightings is not None: + store = storage.Store(hass, "1", f"octopus_energy.{mpan}_rate_weightings") + await store.async_save(RateWeightings(weightings=weightings).dict()) + _LOGGER.debug(f"Saved rate weightings data for {mpan}") \ No newline at end of file diff --git a/custom_components/octopus_energy/target_rates/rolling_target_rate.py b/custom_components/octopus_energy/target_rates/rolling_target_rate.py index 80d88315..abc45c5b 100644 --- a/custom_components/octopus_energy/target_rates/rolling_target_rate.py +++ b/custom_components/octopus_energy/target_rates/rolling_target_rate.py @@ -1,4 +1,3 @@ -from decimal import Decimal import logging from datetime import timedelta import math @@ -13,9 +12,6 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.util.dt import (utcnow, now) -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity -) from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) @@ -30,6 +26,7 @@ CONFIG_TARGET_HOURS_MODE, CONFIG_TARGET_MAX_RATE, CONFIG_TARGET_MIN_RATE, + CONFIG_TARGET_MPAN, CONFIG_TARGET_NAME, CONFIG_TARGET_HOURS, CONFIG_TARGET_TYPE, @@ -41,6 +38,7 @@ CONFIG_TARGET_TYPE_INTERMITTENT, CONFIG_TARGET_WEIGHTING, DATA_ACCOUNT, + DATA_CUSTOM_RATE_WEIGHTINGS_KEY, DOMAIN, ) @@ -59,6 +57,7 @@ from ..utils.attributes import dict_to_typed_dict from ..coordinators import MultiCoordinatorEntity from ..coordinators.free_electricity_sessions import FreeElectricitySessionsCoordinatorResult +from ..utils.weightings import apply_weighting from ..config.rolling_target_rates import validate_rolling_target_rate_config @@ -193,6 +192,13 @@ def _handle_coordinator_update(self) -> None: self._config[CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING] if CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING in self._config else 1 ) + weightings_key = DATA_CUSTOM_RATE_WEIGHTINGS_KEY.format(self._config[CONFIG_TARGET_MPAN]) + applicable_rates = apply_weighting( + applicable_rates, + self._hass.data[DOMAIN][self._account_id][weightings_key] + if weightings_key in self._hass.data[DOMAIN][self._account_id] + else [] + ) if applicable_rates is not None: number_of_slots = math.ceil(target_hours * 2) diff --git a/custom_components/octopus_energy/target_rates/target_rate.py b/custom_components/octopus_energy/target_rates/target_rate.py index d7ce8ec0..04b05c29 100644 --- a/custom_components/octopus_energy/target_rates/target_rate.py +++ b/custom_components/octopus_energy/target_rates/target_rate.py @@ -1,4 +1,3 @@ -from decimal import Decimal import logging from datetime import timedelta import math @@ -13,9 +12,6 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.util.dt import (utcnow, now) -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity -) from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) @@ -29,6 +25,7 @@ CONFIG_TARGET_HOURS_MODE, CONFIG_TARGET_MAX_RATE, CONFIG_TARGET_MIN_RATE, + CONFIG_TARGET_MPAN, CONFIG_TARGET_NAME, CONFIG_TARGET_HOURS, CONFIG_TARGET_OLD_END_TIME, @@ -48,6 +45,7 @@ CONFIG_TARGET_TYPE_INTERMITTENT, CONFIG_TARGET_WEIGHTING, DATA_ACCOUNT, + DATA_CUSTOM_RATE_WEIGHTINGS_KEY, DOMAIN, ) @@ -67,6 +65,7 @@ from ..utils.attributes import dict_to_typed_dict from ..coordinators import MultiCoordinatorEntity from ..coordinators.free_electricity_sessions import FreeElectricitySessionsCoordinatorResult +from ..utils.weightings import apply_weighting _LOGGER = logging.getLogger(__name__) @@ -213,6 +212,14 @@ def _handle_coordinator_update(self) -> None: self._config[CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING] if CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING in self._config else 1 ) + weightings_key = DATA_CUSTOM_RATE_WEIGHTINGS_KEY.format(self._config[CONFIG_TARGET_MPAN]) + applicable_rates = apply_weighting( + applicable_rates, + self._hass.data[DOMAIN][self._account_id][weightings_key] + if weightings_key in self._hass.data[DOMAIN][self._account_id] + else [] + ) + if applicable_rates is not None: number_of_slots = math.ceil(target_hours * 2) weighting = create_weighting(self._config[CONFIG_TARGET_WEIGHTING] if CONFIG_TARGET_WEIGHTING in self._config else None, number_of_slots) diff --git a/custom_components/octopus_energy/translations/en.json b/custom_components/octopus_energy/translations/en.json index 0475f6e9..16faaf52 100644 --- a/custom_components/octopus_energy/translations/en.json +++ b/custom_components/octopus_energy/translations/en.json @@ -275,6 +275,9 @@ }, "octoplus_points_maximum_points": { "message": "You cannot redeem more than {redeemable_points} points" + }, + "invalid_rate_weightings": { + "message": "{error}" } }, "issues": { diff --git a/custom_components/octopus_energy/utils/weightings.py b/custom_components/octopus_energy/utils/weightings.py new file mode 100644 index 00000000..4d65a857 --- /dev/null +++ b/custom_components/octopus_energy/utils/weightings.py @@ -0,0 +1,117 @@ +from datetime import datetime, timedelta + +from pydantic import BaseModel + +class RateWeighting(BaseModel): + start: datetime + end: datetime + weighting: float + +class ValidateRateWeightingsResult: + + def __init__(self, success: bool, weightings: list[RateWeighting] = [], error_message: str | None = None): + self.success = success + self.weightings = weightings + self.error_message = error_message + +def validate_rate_weightings(weightings: list[dict]): + if weightings is None or len(weightings) < 1: + return ValidateRateWeightingsResult(True, []) + + processed_weightings = [] + for index in range(len(weightings)): + weighting = weightings[index] + error = None + + start = None + try: + start = datetime.fromisoformat(weighting["start"]) + except: + error = f"start was not a valid ISO datetime in string format at index {index}" + break + + if start.tzinfo is None: + error = f"start must include timezone at index {index}" + break + + end = None + try: + end = datetime.fromisoformat(weighting["end"]) + except: + error = f"end was not a valid ISO datetime in string format at index {index}" + break + + if end.tzinfo is None: + error = f"end must include timezone at index {index}" + break + + + if start >= end: + error = f"start must be before end at index {index}" + break + + if (end - start).seconds != 1800: # 30 minutes + error = f"time period must be equal to 30 minutes at index {index}" + break + + error = _validate_time(start, "start", index) + if error is not None: + break + + error = _validate_time(end, "end", index) + if error is not None: + break + + processed_weightings.append(RateWeighting(start=start, end=end, weighting=weighting["weighting"])) + + if error is not None: + return ValidateRateWeightingsResult(False, [], error) + + return ValidateRateWeightingsResult(True, processed_weightings) + +def _validate_time(value: datetime, key: str, index: int): + if value.minute != 0 and value.minute != 30: + return f"{key} minute must equal 0 or 30 at index {index}" + + if value.second != 0 or value.microsecond != 0: + return f"{key} second and microsecond must equal 0 at index {index}" + + return None + +def merge_weightings(current_date: datetime, new_weightings: list[RateWeighting], current_weightings: list[RateWeighting]): + merged_weightings: list[RateWeighting] = [] + + if new_weightings is not None: + merged_weightings.extend(new_weightings) + + minimum_date = current_date - timedelta(hours=24) + + if current_weightings is not None: + for weighting in current_weightings: + if weighting.end >= minimum_date: + is_present = False + for existing_weighting in merged_weightings: + if existing_weighting.start == weighting.start and existing_weighting.end == weighting.end: + is_present = True + break + + if is_present == False: + merged_weightings.append(weighting) + + merged_weightings.sort(key=lambda x: x.start) + + return merged_weightings + +def apply_weighting(applicable_rates: list | None, rate_weightings: list[RateWeighting] | None): + if applicable_rates is None: + return None + + if rate_weightings is None: + return applicable_rates + + for rate in applicable_rates: + for session in rate_weightings: + if rate["start"] >= session.start and rate["end"] <= session.end: + rate["weighting"] = session.weighting + + return applicable_rates \ No newline at end of file diff --git a/requirements.test.txt b/requirements.test.txt index d85c8581..7f62827f 100644 --- a/requirements.test.txt +++ b/requirements.test.txt @@ -4,6 +4,7 @@ pytest-asyncio mock homeassistant psutil-home-assistant +pydantic sqlalchemy fnvhash fnv_hash_fast \ No newline at end of file diff --git a/tests/unit/utils/test_apply_weighting.py b/tests/unit/utils/test_apply_weighting.py new file mode 100644 index 00000000..4de4517c --- /dev/null +++ b/tests/unit/utils/test_apply_weighting.py @@ -0,0 +1,51 @@ +from datetime import datetime, timedelta +from decimal import Decimal +import pytest + +from tests.unit import create_rate_data +from custom_components.octopus_energy.utils.weightings import RateWeighting, apply_weighting + +period_from = datetime.strptime("2024-11-26T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") +period_to = datetime.strptime("2024-11-27T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + +@pytest.mark.asyncio +async def test_when_applicable_rates_is_none_then_none_is_returned(): + applicable_rates = None + rate_weightings: list[RateWeighting] = [] + + new_applicable_rates = apply_weighting(applicable_rates, rate_weightings) + assert new_applicable_rates is None + +@pytest.mark.asyncio +async def test_when_rate_weightings_is_none_then_applicable_rates_is_returned(): + applicable_rates = create_rate_data(period_from, period_to, [1, 2]) + rate_weightings: list[RateWeighting] = None + + new_applicable_rates = apply_weighting(applicable_rates, rate_weightings) + + assert new_applicable_rates == applicable_rates + for rate in new_applicable_rates: + assert "weighting" not in rate + +@pytest.mark.asyncio +async def test_when_rate_weightings_is_available_then_weighting_is_added(): + applicable_rates = create_rate_data(period_from, period_to, [1, 2]) + + free_electricity_period_from = datetime.strptime("2024-11-26T10:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + free_electricity_period_to = datetime.strptime("2024-11-26T11:30:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + expected_weighting = Decimal(1.5) + rate_weightings: list[RateWeighting] = [ + RateWeighting(start=free_electricity_period_from, end=free_electricity_period_to, weighting=expected_weighting) + ] + + new_applicable_rates = apply_weighting(applicable_rates, rate_weightings) + + assert new_applicable_rates is not None + for rate in new_applicable_rates: + if (rate["start"] == datetime.strptime("2024-11-26T10:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") or + rate["start"] == datetime.strptime("2024-11-26T10:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + timedelta(minutes=30) or + rate["start"] == datetime.strptime("2024-11-26T10:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + timedelta(minutes=60)): + assert "weighting" in rate + assert rate["weighting"] == expected_weighting + else: + assert "weighting" not in rate \ No newline at end of file diff --git a/tests/unit/utils/test_merge_weightings.py b/tests/unit/utils/test_merge_weightings.py new file mode 100644 index 00000000..a25fb3c9 --- /dev/null +++ b/tests/unit/utils/test_merge_weightings.py @@ -0,0 +1,98 @@ +from datetime import datetime, timedelta +from decimal import Decimal +import pytest + +from custom_components.octopus_energy.utils.weightings import RateWeighting, merge_weightings + +def create_weightings(start: datetime, end: datetime, weighting: Decimal): + weightings = [] + current = start + while current < end: + weightings.append(RateWeighting(start=current, end=current + timedelta(minutes=30), weighting=weighting)) + current = current + timedelta(minutes=30) + + return weightings + +@pytest.mark.asyncio +async def test_when_new_weightings_is_none_then_current_weightings_returned(): + current_date = datetime.strptime("2024-12-24T10:16:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + new_weightings: list[RateWeighting] = None + current_weightings: list[RateWeighting] = [] + + merged_weightings = merge_weightings(current_date, new_weightings, current_weightings) + assert merged_weightings == [] + +@pytest.mark.asyncio +async def test_when_current_weightings_is_none_then_new_weightings_returned(): + current_date = datetime.strptime("2024-12-24T10:16:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + new_weightings: list[RateWeighting] = [] + current_weightings: list[RateWeighting] = None + + merged_weightings = merge_weightings(current_date, new_weightings, current_weightings) + assert merged_weightings == [] + +@pytest.mark.asyncio +async def test_when_new_weightings_in_past_then_weightings_returned(): + current_date = datetime.strptime("2024-12-24T10:16:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + expected_weighting = 1.5 + new_weightings: list[RateWeighting] = create_weightings( + datetime.strptime("2024-12-22T10:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"), + datetime.strptime("2024-12-22T11:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"), + expected_weighting + ) + current_weightings: list[RateWeighting] = None + + merged_weightings = merge_weightings(current_date, new_weightings, current_weightings) + assert merged_weightings == new_weightings + +@pytest.mark.asyncio +async def test_when_current_weightings_in_past_then_current_weightings_more_than_24_hours_removed(): + current_date = datetime.strptime("2024-12-24T10:16:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + expected_weighting = 1.5 + new_weightings: list[RateWeighting] = [] + current_weightings: list[RateWeighting] = create_weightings( + datetime.strptime("2024-12-23T09:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"), + datetime.strptime("2024-12-23T11:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"), + expected_weighting + ) + + merged_weightings = merge_weightings(current_date, new_weightings, current_weightings) + assert len(current_weightings) == 4 + assert len(merged_weightings) == 2 + + assert merged_weightings[0].start == datetime.strptime("2024-12-23T10:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + assert merged_weightings[0].end == datetime.strptime("2024-12-23T10:30:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + assert merged_weightings[0].weighting == expected_weighting + + assert merged_weightings[1].start == datetime.strptime("2024-12-23T10:30:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + assert merged_weightings[1].end == datetime.strptime("2024-12-23T11:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + assert merged_weightings[1].weighting == expected_weighting + +@pytest.mark.asyncio +async def test_when_new_weightings_and_current_weightings_exist_for_same_period_then_new_weighting_wins(): + current_date = datetime.strptime("2024-12-24T10:16:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + expected_weighting = 1.5 + new_weightings: list[RateWeighting] = create_weightings( + datetime.strptime("2024-12-24T10:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"), + datetime.strptime("2024-12-24T11:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"), + expected_weighting + ) + current_weightings: list[RateWeighting] = create_weightings( + datetime.strptime("2024-12-24T09:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"), + datetime.strptime("2024-12-24T10:30:00+00:00", "%Y-%m-%dT%H:%M:%S%z"), + expected_weighting + 0.5 + ) + + merged_weightings = merge_weightings(current_date, new_weightings, current_weightings) + assert len(merged_weightings) == 4 + + current = datetime.strptime("2024-12-24T09:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") + for i in range(len(merged_weightings)): + assert merged_weightings[i].start == current + current += timedelta(minutes=30) + assert merged_weightings[i].end == current + + if merged_weightings[i].start >= datetime.strptime("2024-12-24T10:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"): + assert merged_weightings[i].weighting == expected_weighting + else: + assert merged_weightings[i].weighting != expected_weighting \ No newline at end of file diff --git a/tests/unit/utils/test_validate_rate_weightings.py b/tests/unit/utils/test_validate_rate_weightings.py new file mode 100644 index 00000000..deba5747 --- /dev/null +++ b/tests/unit/utils/test_validate_rate_weightings.py @@ -0,0 +1,184 @@ +from datetime import datetime, timedelta +import pytest + +from custom_components.octopus_energy.utils.weightings import validate_rate_weightings + +@pytest.mark.asyncio +async def test_when_weightings_is_none_then_empty_list_returned(): + weightings: list[dict] = None + + result = validate_rate_weightings(weightings) + assert result.success == True + assert result.weightings == [] + +@pytest.mark.asyncio +async def test_when_weightings_is_empty_list_then_empty_list_returned(): + weightings: list[dict] = [] + + result = validate_rate_weightings(weightings) + assert result.success == True + assert result.weightings == [] + +@pytest.mark.asyncio +@pytest.mark.parametrize("start",[ + ("A"), + ("2024-13-01T00:00:00Z"), + ("2024-12-32T00:00:00Z"), + ("2024-12-24T24:00:00Z"), + ("2024-12-24T23:60:00Z"), + ("2024-12-24T23:00:60Z"), +]) +async def test_when_start_is_not_valid_iso_datetime_then_error_is_returned(start: str): + weightings: list[dict] = [ + { + "start": start, + "end": "2024-12-24T00:30:00Z", + "weighting": 1.5 + } + ] + + result = validate_rate_weightings(weightings) + assert result.success == False + assert result.error_message == "start was not a valid ISO datetime in string format at index 0" + +@pytest.mark.asyncio +async def test_when_start_does_not_contain_timezone_then_error_is_returned(): + weightings: list[dict] = [ + { + "start": "2024-12-24T00:00:00", + "end": "2024-12-24T00:30:00Z", + "weighting": 1.5 + } + ] + + result = validate_rate_weightings(weightings) + assert result.success == False + assert result.error_message == "start must include timezone at index 0" + +@pytest.mark.asyncio +@pytest.mark.parametrize("end",[ + ("A"), + ("2024-13-01T00:00:00Z"), + ("2024-12-32T00:00:00Z"), + ("2024-12-24T24:00:00Z"), + ("2024-12-24T23:60:00Z"), + ("2024-12-24T23:00:60Z"), +]) +async def test_when_end_is_not_valid_iso_datetime_then_error_is_returned(end: str): + weightings: list[dict] = [ + { + "start": "2024-12-24T00:00:00Z", + "end": end, + "weighting": 1.5 + } + ] + + result = validate_rate_weightings(weightings) + assert result.success == False + assert result.error_message == "end was not a valid ISO datetime in string format at index 0" + +@pytest.mark.asyncio +async def test_when_end_does_not_contain_timezone_then_error_is_returned(): + weightings: list[dict] = [ + { + "start": "2024-12-24T00:00:00Z", + "end": "2024-12-24T00:30:00", + "weighting": 1.5 + } + ] + + result = validate_rate_weightings(weightings) + assert result.success == False + assert result.error_message == "end must include timezone at index 0" + +@pytest.mark.asyncio +async def test_when_end_is_before_start_then_error_is_returned(): + weightings: list[dict] = [ + { + "start": "2024-12-24T00:30:00Z", + "end": "2024-12-24T00:29:59Z", + "weighting": 1.5 + } + ] + + result = validate_rate_weightings(weightings) + assert result.success == False + assert result.error_message == "start must be before end at index 0" + +@pytest.mark.asyncio +@pytest.mark.parametrize("end",[ + ("2024-12-24T00:29:59Z"), + ("2024-12-24T00:30:01Z"), +]) +async def test_when_time_period_is_not_thirty_minutes_then_error_is_returned(end: str): + weightings: list[dict] = [ + { + "start": "2024-12-24T00:00:00Z", + "end": end, + "weighting": 1.5 + } + ] + + result = validate_rate_weightings(weightings) + assert result.success == False + assert result.error_message == "time period must be equal to 30 minutes at index 0" + +@pytest.mark.asyncio +async def test_when_start_minute_is_not_valid_then_error_is_returned(): + + for minute in range(60): + if minute == 0 or minute == 30: + continue + + weightings: list[dict] = [ + { + "start": f"2024-12-24T00:{minute:02}:00Z", + "end": (datetime.strptime("2024-12-24T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + timedelta(minutes=30 + minute)).isoformat(), + "weighting": 1.5 + } + ] + + print(weightings[0]["start"]) + result = validate_rate_weightings(weightings) + assert result.success == False + assert result.error_message == "start minute must equal 0 or 30 at index 0" + +@pytest.mark.asyncio +@pytest.mark.parametrize("start,end",[ + ("2024-12-24T00:00:01Z", "2024-12-24T00:30:01Z"), + ("2024-12-24T00:00:00.1Z", "2024-12-24T00:30:00.1Z"), +]) +async def test_when_time_period_is_not_thirty_minutes_then_error_is_returned(start: str, end: str): + weightings: list[dict] = [ + { + "start": start, + "end": end, + "weighting": 1.5 + } + ] + + result = validate_rate_weightings(weightings) + assert result.success == False + assert result.error_message == "start second and microsecond must equal 0 at index 0" + +@pytest.mark.asyncio +@pytest.mark.parametrize("start,end", [ + ("2024-12-24T00:00:00Z", "2024-12-24T00:30:00Z"), + ("2024-12-24T00:30:00Z", "2024-12-24T01:00:00Z"), +]) +async def test_when_data_is_valid_then_success_is_returned(start: str, end: str): + weightings: list[dict] = [ + { + "start": start, + "end": end, + "weighting": 1.5 + } + ] + + result = validate_rate_weightings(weightings) + assert result.success == True + assert len(result.weightings) == 1 + + assert result.weightings[0].start == datetime.strptime(start, "%Y-%m-%dT%H:%M:%S%z") + assert result.weightings[0].end == datetime.strptime(end, "%Y-%m-%dT%H:%M:%S%z") + assert result.weightings[0].weighting == weightings[0]["weighting"] \ No newline at end of file From 05db8c2ffcbdc39341a6e419c77bf9113a6aebe6 Mon Sep 17 00:00:00 2001 From: David Kendall Date: Tue, 24 Dec 2024 20:26:33 +0000 Subject: [PATCH 04/12] feat: Updated target rates to support additional re-evaluation modes for target times. This is to assist with external weightings changing (30 minutes dev time) --- _docs/setup/target_rate.md | 19 +++++ .../octopus_energy/config_flow.py | 71 ++++++++++++------- custom_components/octopus_energy/const.py | 10 +-- .../octopus_energy/target_rates/__init__.py | 8 +-- .../target_rates/rolling_target_rate.py | 4 +- .../target_rates/target_rate.py | 5 +- .../octopus_energy/translations/en.json | 6 +- .../test_should_evaluate_target_rates.py | 36 +++++----- 8 files changed, 100 insertions(+), 59 deletions(-) diff --git a/_docs/setup/target_rate.md b/_docs/setup/target_rate.md index d8eb6fec..10498cfe 100644 --- a/_docs/setup/target_rate.md +++ b/_docs/setup/target_rate.md @@ -84,6 +84,24 @@ The target rate sensor will try to find the best times for the specified hours. For instance if the cheapest period is between `2023-01-01T00:30` and `2023-01-01T05:00` and your target rate is for 1 hour, then it will come on between `2023-01-01T00:30` and `2023-01-01T01:30`. If the available times are between `2023-01-01T00:30` and `2023-01-01T01:00`, then the sensor will come on between `2023-01-01T00:30` and `2023-01-01T01:00`. +### Evaluation mode + +Because the time frame that is being evaluated could have external factors change the underlying data (e.g. if you're using [external rate weightings](#external-rate-weightings)), you might want to set how/when the target times are evaluated in order to make the selected times more or less dynamic. + +#### All existing target rates are in the past + +This is the default way of evaluating target times. This will only evaluate new target times if no target times have been calculated or all existing target times are in the past. + +#### Existing target rates haven't started or finished + +This will only evaluate target times if no target times have been calculated or all existing target times are either in the future or all existing target times are in the past. + +For example, lets say we have a continuous target which looks between `00:00` and `08:00` has existing target times from `2023-01-02T01:00` to `2023-01-02T02:00`. + +* If the current time is `2023-01-02T00:59`, then the target times will be re-evaluated and might change if the target period (i.e. `2023-01-02T00:30` to `2023-01-02T08:30`) has better rates than the existing target times (e.g. the external weightings have changed). +* If the current time is `2023-01-02T01:00`, the the target times will not be re-evaluated because we've entered our current target times, even if the evaluation period has cheaper times. +* If the current time is `2023-01-02T02:01`, the the target times will be re-evaluated because our existing target times are in the past and will find the best times in the new rolling target period (i.e. `2023-01-02T02:00` to `2023-01-02T10:00`). + ### Offset 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. This will shift when the sensor turns on relative to the optimum period. For example, if the optimum period is between `2023-01-18T10:00` and `2023-01-18T11:00` with an offset of `-00:30:00`, the sensor will turn on between `2023-01-18T09:30` and `2023-01-18T10:30`. @@ -178,6 +196,7 @@ The following attributes are available on each sensor | `hours` | `string` | The total hours are being discovered. | | `type` | `string` | The type/mode for the target rate sensor. This will be either `continuous` or `intermittent`. | | `mpan` | `string` | The `mpan` of the meter being used to determine the rates. | +| `target_times_evaluation_mode` | `string` | The mode that determines when/how target times are picked | | `rolling_target` | `boolean` | Determines if `Re-evaluate multiple times a day` is turned on for the sensor. | | `last_rates` | `boolean` | Determines if `Find last applicable rates` is turned off for the sensor. | | `offset` | `string` | The offset configured for the sensor. | diff --git a/custom_components/octopus_energy/config_flow.py b/custom_components/octopus_energy/config_flow.py index 259c1295..bc2fa4ea 100644 --- a/custom_components/octopus_energy/config_flow.py +++ b/custom_components/octopus_energy/config_flow.py @@ -21,10 +21,10 @@ CONFIG_MAIN_HOME_PRO_ADDRESS, CONFIG_MAIN_HOME_PRO_API_KEY, CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD, - CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE, - CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, - CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, - CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS, + CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE, + 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_FREE_ELECTRICITY_WEIGHTING, CONFIG_TARGET_HOURS_MODE, CONFIG_TARGET_HOURS_MODE_EXACT, @@ -231,6 +231,15 @@ async def __async_setup_target_rate_schema__(self, account_id: str): vol.Optional(CONFIG_TARGET_START_TIME): str, vol.Optional(CONFIG_TARGET_END_TIME): str, vol.Optional(CONFIG_TARGET_OFFSET): str, + vol.Required(CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE, default=CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[ + selector.SelectOptionDict(value=CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, label="All existing target rates are in the past"), + selector.SelectOptionDict(value=CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, label="Existing target rates haven't started or finished"), + ], + mode=selector.SelectSelectorMode.DROPDOWN, + ) + ), vol.Optional(CONFIG_TARGET_ROLLING_TARGET, default=False): bool, vol.Optional(CONFIG_TARGET_LAST_RATES, default=False): bool, vol.Optional(CONFIG_TARGET_INVERT_TARGET_RATES, default=False): bool, @@ -278,22 +287,22 @@ async def __async_setup_rolling_target_rate_schema__(self, account_id: str): ), vol.Required(CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD): str, vol.Optional(CONFIG_TARGET_OFFSET): str, - vol.Optional(CONFIG_TARGET_LAST_RATES): bool, - vol.Optional(CONFIG_TARGET_INVERT_TARGET_RATES): bool, - vol.Optional(CONFIG_TARGET_MIN_RATE): str, - vol.Optional(CONFIG_TARGET_MAX_RATE): str, - vol.Optional(CONFIG_TARGET_WEIGHTING): str, - vol.Required(CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING, default=1): cv.positive_float, - vol.Required(CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE, default=CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST): selector.SelectSelector( + vol.Required(CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE, default=CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST): selector.SelectSelector( selector.SelectSelectorConfig( options=[ - selector.SelectOptionDict(value=CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, label="All existing target rates are in the past"), - selector.SelectOptionDict(value=CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, label="Existing target rates haven't started or finished"), - selector.SelectOptionDict(value=CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS, label="Always"), + selector.SelectOptionDict(value=CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, label="All existing target rates are in the past"), + selector.SelectOptionDict(value=CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, label="Existing target rates haven't started or finished"), + selector.SelectOptionDict(value=CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS, label="Always"), ], mode=selector.SelectSelectorMode.DROPDOWN, ) ), + vol.Optional(CONFIG_TARGET_LAST_RATES): bool, + vol.Optional(CONFIG_TARGET_INVERT_TARGET_RATES): bool, + vol.Optional(CONFIG_TARGET_MIN_RATE): str, + vol.Optional(CONFIG_TARGET_MAX_RATE): str, + vol.Optional(CONFIG_TARGET_WEIGHTING): str, + vol.Required(CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING, default=1): cv.positive_float, }) async def __async_setup_cost_tracker_schema__(self, account_id: str): @@ -621,6 +630,15 @@ async def __async_setup_target_rate_schema__(self, config, errors): vol.Optional(CONFIG_TARGET_START_TIME): str, vol.Optional(CONFIG_TARGET_END_TIME): str, vol.Optional(CONFIG_TARGET_OFFSET): str, + vol.Required(CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[ + selector.SelectOptionDict(value=CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, label="All existing target rates are in the past"), + selector.SelectOptionDict(value=CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, label="Existing target rates haven't started or finished"), + ], + mode=selector.SelectSelectorMode.DROPDOWN, + ) + ), vol.Optional(CONFIG_TARGET_ROLLING_TARGET): bool, vol.Optional(CONFIG_TARGET_LAST_RATES): bool, vol.Optional(CONFIG_TARGET_INVERT_TARGET_RATES): bool, @@ -644,7 +662,8 @@ async def __async_setup_target_rate_schema__(self, config, errors): CONFIG_TARGET_MIN_RATE: f'{config[CONFIG_TARGET_MIN_RATE]}' if CONFIG_TARGET_MIN_RATE in config and config[CONFIG_TARGET_MIN_RATE] is not None else None, CONFIG_TARGET_MAX_RATE: f'{config[CONFIG_TARGET_MAX_RATE]}' if CONFIG_TARGET_MAX_RATE in config and config[CONFIG_TARGET_MAX_RATE] is not None else None, CONFIG_TARGET_WEIGHTING: config[CONFIG_TARGET_WEIGHTING] if CONFIG_TARGET_WEIGHTING in config else None, - CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING: config[CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING] if CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING in config else 1 + CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING: config[CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING] if CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING in config else 1, + CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE: config[CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE] if CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE in config else CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, } ), errors=errors @@ -709,22 +728,22 @@ async def __async_setup_rolling_target_rate_schema__(self, config, errors): ), vol.Required(CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD): str, vol.Optional(CONFIG_TARGET_OFFSET): str, - vol.Optional(CONFIG_TARGET_LAST_RATES): bool, - vol.Optional(CONFIG_TARGET_INVERT_TARGET_RATES): bool, - vol.Optional(CONFIG_TARGET_MIN_RATE): str, - vol.Optional(CONFIG_TARGET_MAX_RATE): str, - vol.Optional(CONFIG_TARGET_WEIGHTING): str, - vol.Required(CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING): cv.positive_float, - vol.Required(CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE, default=CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST): selector.SelectSelector( + vol.Required(CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE, default=CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST): selector.SelectSelector( selector.SelectSelectorConfig( options=[ - selector.SelectOptionDict(value=CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, label="All existing target rates are in the past"), - selector.SelectOptionDict(value=CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, label="Existing target rates haven't started or finished"), - selector.SelectOptionDict(value=CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS, label="Always"), + selector.SelectOptionDict(value=CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, label="All existing target rates are in the past"), + selector.SelectOptionDict(value=CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, label="Existing target rates haven't started or finished"), + selector.SelectOptionDict(value=CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS, label="Always"), ], mode=selector.SelectSelectorMode.DROPDOWN, ) ), + vol.Optional(CONFIG_TARGET_LAST_RATES): bool, + vol.Optional(CONFIG_TARGET_INVERT_TARGET_RATES): bool, + vol.Optional(CONFIG_TARGET_MIN_RATE): str, + vol.Optional(CONFIG_TARGET_MAX_RATE): str, + vol.Optional(CONFIG_TARGET_WEIGHTING): str, + vol.Required(CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING): cv.positive_float, }), { CONFIG_TARGET_NAME: config[CONFIG_TARGET_NAME], @@ -740,7 +759,7 @@ async def __async_setup_rolling_target_rate_schema__(self, config, errors): CONFIG_TARGET_MIN_RATE: f'{config[CONFIG_TARGET_MIN_RATE]}' if CONFIG_TARGET_MIN_RATE in config and config[CONFIG_TARGET_MIN_RATE] is not None else None, CONFIG_TARGET_MAX_RATE: f'{config[CONFIG_TARGET_MAX_RATE]}' if CONFIG_TARGET_MAX_RATE in config and config[CONFIG_TARGET_MAX_RATE] is not None else None, CONFIG_TARGET_WEIGHTING: config[CONFIG_TARGET_WEIGHTING] if CONFIG_TARGET_WEIGHTING in config else None, - CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE: config[CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE] if CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE in config else CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, + CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE: config[CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE] if CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE in config else CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING: config[CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING] if CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING in config else 1 } ), diff --git a/custom_components/octopus_energy/const.py b/custom_components/octopus_energy/const.py index efbda05e..b8e5b10f 100644 --- a/custom_components/octopus_energy/const.py +++ b/custom_components/octopus_energy/const.py @@ -71,12 +71,12 @@ CONFIG_TARGET_MAX_RATE = "maximum_rate" CONFIG_TARGET_WEIGHTING = "weighting" CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING = "free_electricity_weighting" +CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE = "target_times_evaluation_mode" +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_ROLLING_TARGET_HOURS_LOOK_AHEAD = "look_ahead_hours" -CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE = "target_times_evaluation_mode" -CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST = "all_target_times_in_past" -CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST = "all_target_times_in_future_or_past" -CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS = "always" CONFIG_TARGET_KEYS = [ CONFIG_TARGET_NAME, @@ -94,7 +94,7 @@ CONFIG_TARGET_WEIGHTING, CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING, CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD, - CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE + CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE ] CONFIG_COST_TRACKER_NAME = "name" diff --git a/custom_components/octopus_energy/target_rates/__init__.py b/custom_components/octopus_energy/target_rates/__init__.py index ec6a96e3..f0a85739 100644 --- a/custom_components/octopus_energy/target_rates/__init__.py +++ b/custom_components/octopus_energy/target_rates/__init__.py @@ -7,7 +7,7 @@ from homeassistant.util.dt import (as_utc, parse_datetime) from ..utils.conversions import value_inc_vat_to_pounds -from ..const import CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, CONFIG_ROLLING_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_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 ..api_client.free_electricity_sessions import FreeElectricitySession _LOGGER = logging.getLogger(__name__) @@ -419,9 +419,9 @@ def should_evaluate_target_rates(current_date: datetime, target_rates: list, eva if rate["start"] <= current_date: one_rate_in_past = True - return ((evaluation_mode == CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST and all_rates_in_past) or - (evaluation_mode == CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST and (one_rate_in_past == False or all_rates_in_past)) or - (evaluation_mode == CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS)) + return ((evaluation_mode == CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST and all_rates_in_past) or + (evaluation_mode == CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST and (one_rate_in_past == False or all_rates_in_past)) or + (evaluation_mode == CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS)) def apply_free_electricity_weighting(applicable_rates: list | None, free_electricity_sessions: list[FreeElectricitySession] | None, weighting: float): if applicable_rates is None: diff --git a/custom_components/octopus_energy/target_rates/rolling_target_rate.py b/custom_components/octopus_energy/target_rates/rolling_target_rate.py index abc45c5b..3ae3d2bb 100644 --- a/custom_components/octopus_energy/target_rates/rolling_target_rate.py +++ b/custom_components/octopus_energy/target_rates/rolling_target_rate.py @@ -21,7 +21,7 @@ from ..const import ( CONFIG_ROLLING_TARGET_HOURS_LOOK_AHEAD, - CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE, + CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE, CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING, CONFIG_TARGET_HOURS_MODE, CONFIG_TARGET_MAX_RATE, @@ -147,7 +147,7 @@ def _handle_coordinator_update(self) -> None: _LOGGER.debug(f'Updating OctopusEnergyTargetRate {self._config[CONFIG_TARGET_NAME]}') self._last_evaluated = current_date - should_evaluate = should_evaluate_target_rates(current_date, self._target_rates, self._config[CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE]) + should_evaluate = should_evaluate_target_rates(current_date, self._target_rates, self._config[CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE]) if should_evaluate: if self.coordinator is not None and self.coordinator.data is not None and self.coordinator.data.rates is not None: all_rates = self.coordinator.data.rates diff --git a/custom_components/octopus_energy/target_rates/target_rate.py b/custom_components/octopus_energy/target_rates/target_rate.py index 04b05c29..2408604f 100644 --- a/custom_components/octopus_energy/target_rates/target_rate.py +++ b/custom_components/octopus_energy/target_rates/target_rate.py @@ -20,7 +20,8 @@ from homeassistant.helpers import translation from ..const import ( - CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, + CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE, + CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, CONFIG_TARGET_FREE_ELECTRICITY_WEIGHTING, CONFIG_TARGET_HOURS_MODE, CONFIG_TARGET_MAX_RATE, @@ -153,7 +154,7 @@ def _handle_coordinator_update(self) -> None: _LOGGER.debug(f'Updating OctopusEnergyTargetRate {self._config[CONFIG_TARGET_NAME]}') self._last_evaluated = current_date - should_evaluate = should_evaluate_target_rates(current_date, self._target_rates, CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST) + should_evaluate = should_evaluate_target_rates(current_date, self._target_rates, self._config[CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE] if CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE in self._config else CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST) if should_evaluate: if self.coordinator is not None and self.coordinator.data is not None and self.coordinator.data.rates is not None: all_rates = self.coordinator.data.rates diff --git a/custom_components/octopus_energy/translations/en.json b/custom_components/octopus_energy/translations/en.json index 16faaf52..d4f8abe9 100644 --- a/custom_components/octopus_energy/translations/en.json +++ b/custom_components/octopus_energy/translations/en.json @@ -44,7 +44,8 @@ "minimum_rate": "The optional minimum rate for target hours", "maximum_rate": "The optional maximum rate for target hours", "weighting": "The optional weighting to apply to the discovered rates", - "free_electricity_weighting": "The weighting to apply to rates during free electricity sessions" + "free_electricity_weighting": "The weighting to apply to rates during free electricity sessions", + "target_times_evaluation_mode": "When should target times be selected" }, "data_description": { "hours": "This has to be a multiple of 0.5.", @@ -189,7 +190,8 @@ "maximum_rate": "The optional maximum rate for target hours", "rolling_target": "Re-evaluate multiple times a day", "weighting": "The optional weighting to apply to the discovered rates", - "free_electricity_weighting": "The weighting to apply to rates during free electricity sessions" + "free_electricity_weighting": "The weighting to apply to rates during free electricity sessions", + "target_times_evaluation_mode": "When should target times be selected" } }, "rolling_target_rate": { diff --git a/tests/unit/target_rates/test_should_evaluate_target_rates.py b/tests/unit/target_rates/test_should_evaluate_target_rates.py index 8c66eb41..9431088e 100644 --- a/tests/unit/target_rates/test_should_evaluate_target_rates.py +++ b/tests/unit/target_rates/test_should_evaluate_target_rates.py @@ -1,8 +1,8 @@ from datetime import datetime, timedelta from custom_components.octopus_energy.const import ( - CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, - CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, - CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS + 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 ) import pytest @@ -11,9 +11,9 @@ @pytest.mark.asyncio @pytest.mark.parametrize("evaluation_mode",[ - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST), - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST), - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS), ]) async def test_when_target_rates_is_none_then_return_true(evaluation_mode: str): # Arrange @@ -28,9 +28,9 @@ async def test_when_target_rates_is_none_then_return_true(evaluation_mode: str): @pytest.mark.asyncio @pytest.mark.parametrize("evaluation_mode",[ - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST), - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST), - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS), ]) async def test_when_target_rates_is_empty_then_return_true(evaluation_mode: str): # Arrange @@ -45,9 +45,9 @@ async def test_when_target_rates_is_empty_then_return_true(evaluation_mode: str) @pytest.mark.asyncio @pytest.mark.parametrize("evaluation_mode,expected_result",[ - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, False), - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, True), - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS, True), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, False), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, True), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS, True), ]) async def test_when_target_rates_is_in_the_future_then_return_expected_result(evaluation_mode: str, expected_result: bool): # Arrange @@ -66,9 +66,9 @@ async def test_when_target_rates_is_in_the_future_then_return_expected_result(ev @pytest.mark.asyncio @pytest.mark.parametrize("evaluation_mode,expected_result",[ - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, False), - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, False), - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS, True), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, False), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, False), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS, True), ]) async def test_when_target_rates_started_then_return_expected_result(evaluation_mode: str, expected_result: bool): # Arrange @@ -87,9 +87,9 @@ async def test_when_target_rates_started_then_return_expected_result(evaluation_ @pytest.mark.asyncio @pytest.mark.parametrize("evaluation_mode,expected_result",[ - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, True), - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, True), - (CONFIG_ROLLING_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS, True), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_PAST, True), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALL_IN_FUTURE_OR_PAST, True), + (CONFIG_TARGET_TARGET_TIMES_EVALUATION_MODE_ALWAYS, True), ]) async def test_when_target_rates_in_past_then_return_expected_result(evaluation_mode: str, expected_result: bool): # Arrange From 7c3596cc920957dfe7ca23e42828aad826e44c43 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Tue, 24 Dec 2024 20:35:13 +0000 Subject: [PATCH 05/12] feat: Added support for INDRA intelligent provider (5 minutes dev time) --- custom_components/octopus_energy/intelligent/__init__.py | 3 ++- tests/unit/intelligent/test_get_intelligent_features.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/octopus_energy/intelligent/__init__.py b/custom_components/octopus_energy/intelligent/__init__.py index ef0a753e..95f55a90 100644 --- a/custom_components/octopus_energy/intelligent/__init__.py +++ b/custom_components/octopus_energy/intelligent/__init__.py @@ -230,7 +230,8 @@ def __init__(self, "SMARTCAR", "TESLA", "SMART_PEAR", - "HYPERVOLT" + "HYPERVOLT", + "INDRA" ] def get_intelligent_features(provider: str) -> IntelligentFeatures: diff --git a/tests/unit/intelligent/test_get_intelligent_features.py b/tests/unit/intelligent/test_get_intelligent_features.py index d8910e3f..6b2d5f4e 100644 --- a/tests/unit/intelligent/test_get_intelligent_features.py +++ b/tests/unit/intelligent/test_get_intelligent_features.py @@ -20,6 +20,7 @@ ("TESLA", True, True, True, True, True, False), ("SMART_PEAR", True, True, True, True, True, False), ("HYPERVOLT", True, True, True, True, True, False), + ("INDRA", True, True, True, True, True, False), ("OHME", False, False, False, False, False, False), ("DAIKIN".lower(), True, True, True, True, True, False), ("ECOBEE".lower(), True, True, True, True, True, False), @@ -37,6 +38,7 @@ ("TESLA".lower(), True, True, True, True, True, False), ("SMART_PEAR".lower(), True, True, True, True, True, False), ("HYPERVOLT".lower(), True, True, True, True, True, False), + ("INDRA".lower(), True, True, True, True, True, False), ("OHME".lower(), False, False, False, False, False, False), # Unexpected providers ("unexpected".lower(), False, False, False, False, False, True), From 9af97a54b7a9dfb091f4e48f0cd66f758e7e2629 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Wed, 25 Dec 2024 03:40:59 +0000 Subject: [PATCH 06/12] fix: Fixed state class for current total consumption sensors (5 minutes dev time) --- .../electricity/current_total_consumption.py | 7 +------ .../gas/current_total_consumption_cubic_meters.py | 2 +- .../octopus_energy/gas/current_total_consumption_kwh.py | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/custom_components/octopus_energy/electricity/current_total_consumption.py b/custom_components/octopus_energy/electricity/current_total_consumption.py index e1efcbcf..e13c6273 100644 --- a/custom_components/octopus_energy/electricity/current_total_consumption.py +++ b/custom_components/octopus_energy/electricity/current_total_consumption.py @@ -65,7 +65,7 @@ def device_class(self): @property def state_class(self): """The state class of sensor""" - return SensorStateClass.TOTAL + return SensorStateClass.TOTAL_INCREASING @property def native_unit_of_measurement(self): @@ -82,11 +82,6 @@ def extra_state_attributes(self): """Attributes of the sensor.""" return self._attributes - @property - def last_reset(self): - """Return the time when the sensor was last reset, if any.""" - return self._last_reset - @property def native_value(self): return self._state diff --git a/custom_components/octopus_energy/gas/current_total_consumption_cubic_meters.py b/custom_components/octopus_energy/gas/current_total_consumption_cubic_meters.py index 703b967a..c3a308b0 100644 --- a/custom_components/octopus_energy/gas/current_total_consumption_cubic_meters.py +++ b/custom_components/octopus_energy/gas/current_total_consumption_cubic_meters.py @@ -67,7 +67,7 @@ def device_class(self): @property def state_class(self): """The state class of sensor""" - return SensorStateClass.TOTAL + return SensorStateClass.TOTAL_INCREASING @property def native_unit_of_measurement(self): diff --git a/custom_components/octopus_energy/gas/current_total_consumption_kwh.py b/custom_components/octopus_energy/gas/current_total_consumption_kwh.py index 66575d8c..503ded94 100644 --- a/custom_components/octopus_energy/gas/current_total_consumption_kwh.py +++ b/custom_components/octopus_energy/gas/current_total_consumption_kwh.py @@ -67,7 +67,7 @@ def device_class(self): @property def state_class(self): """The state class of sensor""" - return SensorStateClass.TOTAL + return SensorStateClass.TOTAL_INCREASING @property def native_unit_of_measurement(self): From 78748d169887227de1a2f1f8bc73dfd1bf281190 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Wed, 25 Dec 2024 03:44:00 +0000 Subject: [PATCH 07/12] fix: Updated total consumption sensors to ignore zero based results reported by home pro (10 minute dev time) --- .../octopus_energy/electricity/current_total_consumption.py | 2 +- .../gas/current_total_consumption_cubic_meters.py | 4 ++-- .../octopus_energy/gas/current_total_consumption_kwh.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/octopus_energy/electricity/current_total_consumption.py b/custom_components/octopus_energy/electricity/current_total_consumption.py index e13c6273..9e896b42 100644 --- a/custom_components/octopus_energy/electricity/current_total_consumption.py +++ b/custom_components/octopus_energy/electricity/current_total_consumption.py @@ -97,7 +97,7 @@ def _handle_coordinator_update(self) -> None: _LOGGER.debug(f"Calculated total electricity consumption for '{self._mpan}/{self._serial_number}'...") if consumption_data[-1]["total_consumption"] is not None: - self._state = consumption_data[-1]["total_consumption"] + self._state = consumption_data[-1]["total_consumption"] if consumption_data[-1]["total_consumption"] is not None and consumption_data[-1]["total_consumption"] != 0 else None self._last_reset = current self._attributes = { diff --git a/custom_components/octopus_energy/gas/current_total_consumption_cubic_meters.py b/custom_components/octopus_energy/gas/current_total_consumption_cubic_meters.py index c3a308b0..9be19c9e 100644 --- a/custom_components/octopus_energy/gas/current_total_consumption_cubic_meters.py +++ b/custom_components/octopus_energy/gas/current_total_consumption_cubic_meters.py @@ -100,9 +100,9 @@ def _handle_coordinator_update(self) -> None: if consumption_data[-1]["total_consumption"] is not None: if "is_kwh" not in consumption_data[-1] or consumption_data[-1]["is_kwh"] == True: - self._state = convert_kwh_to_m3(consumption_data[-1]["total_consumption"], self._calorific_value) if consumption_data[-1]["total_consumption"] is not None else None + self._state = convert_kwh_to_m3(consumption_data[-1]["total_consumption"], self._calorific_value) if consumption_data[-1]["total_consumption"] is not None and consumption_data[-1]["total_consumption"] != 0 else None else: - self._state = consumption_data[-1]["total_consumption"] + self._state = consumption_data[-1]["total_consumption"] if consumption_data[-1]["total_consumption"] is not None and consumption_data[-1]["total_consumption"] != 0 else None self._attributes = { "mprn": self._mprn, diff --git a/custom_components/octopus_energy/gas/current_total_consumption_kwh.py b/custom_components/octopus_energy/gas/current_total_consumption_kwh.py index 503ded94..6fad52db 100644 --- a/custom_components/octopus_energy/gas/current_total_consumption_kwh.py +++ b/custom_components/octopus_energy/gas/current_total_consumption_kwh.py @@ -100,9 +100,9 @@ def _handle_coordinator_update(self) -> None: if consumption_data[-1]["total_consumption"] is not None: if "is_kwh" not in consumption_data[-1] or consumption_data[-1]["is_kwh"] == True: - self._state = consumption_data[-1]["total_consumption"] + self._state = consumption_data[-1]["total_consumption"] if consumption_data[-1]["total_consumption"] is not None and consumption_data[-1]["total_consumption"] != 0 else None else: - self._state = convert_m3_to_kwh(consumption_data[-1]["total_consumption"], self._calorific_value) if consumption_data[-1]["total_consumption"] is not None else None + self._state = convert_m3_to_kwh(consumption_data[-1]["total_consumption"], self._calorific_value) if consumption_data[-1]["total_consumption"] is not None and consumption_data[-1]["total_consumption"] != 0 else None self._attributes = { "mprn": self._mprn, From b44c4ea9b11fc229b63dac5114821da16df6de38 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Wed, 25 Dec 2024 03:56:16 +0000 Subject: [PATCH 08/12] docs: Updated energy dashboard instructions to include home pro instructions --- .../assets/total_consumption_electricity.png | Bin 0 -> 70252 bytes _docs/assets/total_consumption_gas.png | Bin 0 -> 82052 bytes _docs/setup/energy_dashboard.md | 36 +++++++++++++++++- 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 _docs/assets/total_consumption_electricity.png create mode 100644 _docs/assets/total_consumption_gas.png diff --git a/_docs/assets/total_consumption_electricity.png b/_docs/assets/total_consumption_electricity.png new file mode 100644 index 0000000000000000000000000000000000000000..acbc48e8125a211a235a40b8fc86bf2985238561 GIT binary patch literal 70252 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⁣u3{UYhPld|nfFTLW& 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�nduz(F5tYf7n&> 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 literal 0 HcmV?d00001 diff --git a/_docs/assets/total_consumption_gas.png b/_docs/assets/total_consumption_gas.png new file mode 100644 index 0000000000000000000000000000000000000000..94bc00d2b37935dfb2e93f9a205d48f6c1a15fd0 GIT binary patch literal 82052 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 P20jE=(jkt^uZjNwBR2u> literal 0 HcmV?d00001 diff --git a/_docs/setup/energy_dashboard.md b/_docs/setup/energy_dashboard.md index 14905efe..c9d739c6 100644 --- a/_docs/setup/energy_dashboard.md +++ b/_docs/setup/energy_dashboard.md @@ -21,6 +21,21 @@ If you have an Octopus Home Mini and a smart electricity meter you can obtain li Data will only appear in the energy dashboard from the point you configure the Home Mini within the integration. It doesn't backport any data. +#### Octopus Home Pro + +If you have an Octopus Home Pro and a smart electricity meter you can obtain live meter reading data into Home Assistant: + +1. Go to your [energy dashboard configuration](https://my.home-assistant.io/redirect/config_energy/) +2. Click `Add Consumption` under `Electricity grid` +3. For `Consumed energy` you want `sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_current_total_consumption` +4. Choose the `Use an entity with current price` option and the entity is `sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_current_rate` + +![HA modal electricity example](../assets/total_consumption_electricity.png){: style="height:500px"} + +!!! note + + Data will only appear in the energy dashboard from the point you configure the Home Pro within the integration. It doesn't backport any data. + #### Alternative methods to measure current Home Consumption If you don't have an Octopus Home mini you may have another way to get live or near-live daily consumption into Home Assistant such as a Hildebrand Glow In Home Display, an Energy CT Clamp such as the Shelly EM on the incoming supply cable, or your existing Solar/Battery inverter may have a sensor that provides Grid import information that you can use in the Energy dashboard. @@ -33,19 +48,36 @@ Do be aware that as you are not directly capturing the smart meter readings in H ### For Gas -![HA modal gas example](../assets/current_consumption_gas.png){: style="height:500px"} +#### Octopus Home Mini -This is only available if you have an Octopus Home Mini and a smart gas meter. +If you have an Octopus Home Mini and a smart electricity meter you can obtain live meter reading data into Home Assistant: 1. Go to your [energy dashboard configuration](https://my.home-assistant.io/redirect/config_energy/) 2. Click `Add Gas Source` under `Gas consumption` 3. For `Gas usage` you want `sensor.octopus_energy_gas_{{METER_SERIAL_NUMBER}}_{{MPRN_NUMBER}}_current_accumulative_consumption_kwh` 4. For `Use an entity tracking the total costs` option you want `sensor.octopus_energy_gas_{{METER_SERIAL_NUMBER}}_{{MPRN_NUMBER}}_current_accumulative_cost` +![HA modal gas example](../assets/current_consumption_gas.png){: style="height:500px"} + !!! note Data will only appear in the energy dashboard from the point you configure the Home Mini within the integration. It doesn't backport any data. +#### Octopus Home Pro + +If you have an Octopus Home Pro and a smart gas meter you can obtain live meter reading data into Home Assistant: + +1. Go to your [energy dashboard configuration](https://my.home-assistant.io/redirect/config_energy/) +2. Click `Add Consumption` under `Electricity grid` +3. For `Consumed energy` you want `sensor.octopus_energy_gas_{{METER_SERIAL_NUMBER}}_{{MPRN_NUMBER}}_current_total_consumption_kwh` +4. Choose the `Use an entity with current price` option and the entity is `sensor.octopus_energy_gas_{{METER_SERIAL_NUMBER}}_{{MPRN_NUMBER}}_current_rate` + +![HA modal electricity example](../assets/total_consumption_gas.png){: style="height:500px"} + +!!! note + + Data will only appear in the energy dashboard from the point you configure the Home Pro within the integration. It doesn't backport any data. + ## Previous Day Consumption If none of the methods above for feeding Current Day Consumption information into the Energy dashboard are suitable, you can add `previous consumption` information to the dashboard, using information retrieved via the Octopus API. Note that the consumption information is only available on the following day so "today's" Energy dashboard will show zero values, but "yesterday's", "day before", etc will show the correct consumption for each day. From 91e08a4548c666bfa84ff6c1f9046a888104034e Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Wed, 25 Dec 2024 04:26:51 +0000 Subject: [PATCH 09/12] chore: Updated title for home pro address in config --- custom_components/octopus_energy/translations/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/octopus_energy/translations/en.json b/custom_components/octopus_energy/translations/en.json index d4f8abe9..79c66c64 100644 --- a/custom_components/octopus_energy/translations/en.json +++ b/custom_components/octopus_energy/translations/en.json @@ -13,7 +13,7 @@ "calorific_value": "Gas calorific value.", "electricity_price_cap": "Optional electricity price cap in pence", "gas_price_cap": "Optional gas price cap in pence", - "home_pro_address": "Home Pro address (e.g. http://localhost)", + "home_pro_address": "Home Pro address (e.g. http://192.168.0.1)", "home_pro_api_key": "Home Pro API key", "favour_direct_debit_rates": "Favour direct debit rates where available" }, @@ -161,7 +161,7 @@ "calorific_value": "Gas calorific value", "electricity_price_cap": "Optional electricity price cap in pence", "gas_price_cap": "Optional gas price cap in pence", - "home_pro_address": "Home Pro address (e.g. http://localhost:8000)", + "home_pro_address": "Home Pro address (e.g. http://192.168.0.1)", "home_pro_api_key": "Home Pro API key", "favour_direct_debit_rates": "Favour direct debit rates where available" }, From 3f3a3f7f0975f2f4f2abb5aac049ea76a8c1d4c2 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Wed, 25 Dec 2024 16:04:10 +0000 Subject: [PATCH 10/12] docs: Added strict mode to mkdocs configuration --- .github/workflows/docs.yml | 2 +- mkdocs.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bc537e82..d25e456e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,7 +34,7 @@ jobs: restore-keys: | mkdocs-material- - run: pip install -r requirements.txt - - run: mkdocs build --strict + - run: mkdocs build deploy_docs: if: ${{ github.repository_owner == 'BottlecapDave' && (github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main') }} diff --git a/mkdocs.yml b/mkdocs.yml index d5d234a4..14c1268c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,7 @@ site_name: Home Assistant Octopus Energy repo_url: https://github.com/bottlecapdave/homeassistant-octopusenergy docs_dir: _docs +strict: true nav: - Home: index.md From 06bd74ab1865c2f301abb1788d482d4bc61ebeb8 Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Thu, 26 Dec 2024 06:25:33 +0000 Subject: [PATCH 11/12] chore: Updated docs to fail if links are invalid --- mkdocs.yml | 16 ++++++++++++++-- requirements.txt | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 14c1268c..b36da049 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,6 @@ site_name: Home Assistant Octopus Energy repo_url: https://github.com/bottlecapdave/homeassistant-octopusenergy docs_dir: _docs -strict: true nav: - Home: index.md @@ -72,4 +71,17 @@ theme: primary: light blue toggle: icon: material/brightness-4 - name: Switch to light mode \ No newline at end of file + name: Switch to light mode + +strict: true + +validation: + nav: + omitted_files: warn + not_found: warn + absolute_links: warn + links: + not_found: warn + anchors: warn + absolute_links: warn + unrecognized_links: warn \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f1a3e0c8..3297719f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -mkdocs-material==9.5.27 +mkdocs-material==9.5.49 mike==2.0.0 # mkdocs-git-committers-plugin-2==2.3.0 mkdocs-git-authors-plugin==0.9.0 \ No newline at end of file From 687f6032a4e18832a3cb54fa16d7087b1b2a368c Mon Sep 17 00:00:00 2001 From: BottlecapDave Date: Thu, 26 Dec 2024 06:28:40 +0000 Subject: [PATCH 12/12] docs: Fixed broken links --- _docs/entities/intelligent.md | 4 +- _docs/events.md | 56 ----------------------- _docs/services.md | 2 +- _docs/setup/account.md | 2 +- _docs/setup/tariff_comparison.md | 8 +--- custom_components/octopus_energy/const.py | 2 - 6 files changed, 6 insertions(+), 68 deletions(-) diff --git a/_docs/entities/intelligent.md b/_docs/entities/intelligent.md index 779c504b..68dc2a88 100644 --- a/_docs/entities/intelligent.md +++ b/_docs/entities/intelligent.md @@ -127,5 +127,5 @@ If you're moving to this integration from [megakid/ha_octopus_intelligent](https * `sensor.octopus_intelligent_offpeak_end` - The default off peak end date/time can be found as an attribute on the [off peak sensor](./electricity.md#off-peak). This can be extracted using a [template sensor](https://www.home-assistant.io/integrations/template/). * `switch.octopus_intelligent_bump_charge` - Use the [bump charge sensor](#bump-charge) * `switch.octopus_intelligent_smart_charging` - Use the [smart charge sensor](#smart-charge) -* `select.octopus_intelligent_target_time` - Use the [ready time sensor](#ready-time) -* `select.octopus_intelligent_target_soc` - Use the [charge limit sensor](#charge-limit) \ No newline at end of file +* `select.octopus_intelligent_target_time` - Use the [target time sensor](#target-time) +* `select.octopus_intelligent_target_soc` - Use the [charge target sensor](#charge-target) \ No newline at end of file diff --git a/_docs/events.md b/_docs/events.md index 77c3b9fe..0132367f 100644 --- a/_docs/events.md +++ b/_docs/events.md @@ -126,34 +126,6 @@ This is fired when the [previous consumption's](./entities/electricity.md#previo New rates available for {{ trigger.event.data.mpan }}. Starting value is {{ trigger.event.data.rates[0]["value_inc_vat"] }} ``` -## Electricity Previous Consumption Override Rates - -`octopus_energy_electricity_previous_consumption_override_rates` - -This is fired when the [previous consumption override's](./entities/electricity.md#tariff-overrides) rates are updated. - -| Attribute | Type | Description | -|-----------|------|-------------| -| `rates` | `array` | The list of rates applicable for the previous consumption override | -| `tariff_code` | `string` | The tariff code associated with previous consumption override's rates | -| `mpan` | `string` | The mpan of the meter associated with these rates | -| `serial_number` | `string` | The serial number of the meter associated with these rates | - -### Automation Example - -```yaml -- trigger: - - platform: event - event_type: octopus_energy_electricity_previous_consumption_override_rates - condition: [] - action: - - service: persistent_notification.create - data: - title: "Rates Updated" - message: > - New rates available for {{ trigger.event.data.mpan }}. Starting value is {{ trigger.event.data.rates[0]["value_inc_vat"] }} -``` - ## Gas Current Day Rates `octopus_energy_gas_current_day_rates` @@ -278,34 +250,6 @@ This is fired when the [previous consumption's](./entities/gas.md#previous-accum New rates available for {{ trigger.event.data.mprn }}. Starting value is {{ trigger.event.data.rates[0]["value_inc_vat"] }} ``` -## Gas Previous Consumption Override Rates - -`octopus_energy_gas_previous_consumption_override_rates` - -This is fired when the [previous consumption override's](./entities/gas.md#tariff-overrides) rates are updated. - -| Attribute | Type | Description | -|-----------|------|-------------| -| `rates` | `array` | The list of rates applicable for the previous consumption override | -| `tariff_code` | `string` | The tariff code associated with previous consumption override's rates | -| `mprn` | `string` | The mprn of the meter associated with these rates | -| `serial_number` | `string` | The serial number of the meter associated with these rates | - -### Automation Example - -```yaml -- trigger: - - platform: event - event_type: octopus_energy_gas_previous_consumption_override_rates - condition: [] - action: - - service: persistent_notification.create - data: - title: "Rates Updated" - message: > - New rates available for {{ trigger.event.data.mprn }}. Starting value is {{ trigger.event.data.rates[0]["value_inc_vat"] }} -``` - ## New Saving Session `octopus_energy_new_octoplus_saving_session` diff --git a/_docs/services.md b/_docs/services.md index 30d27cbf..bfd7d827 100644 --- a/_docs/services.md +++ b/_docs/services.md @@ -250,7 +250,7 @@ Rate weightings are added to any existing rate weightings that have been previou ### Automation Example -This automation adds weightings based on the national grids carbon intensity, as provided by [Octopus Energy Carbon Intensity](https://github.com/BottlecapDave/HomeAssistant-CarbonIntensity). +This automation adds weightings based on the national grids carbon intensity, as provided by [Carbon Intensity](https://github.com/BottlecapDave/HomeAssistant-CarbonIntensity). ```yaml - alias: Carbon Intensity Rate Weightings diff --git a/_docs/setup/account.md b/_docs/setup/account.md index 2341ff58..cfd3707b 100644 --- a/_docs/setup/account.md +++ b/_docs/setup/account.md @@ -14,7 +14,7 @@ If you are lucky enough to own an [Octopus Home Mini](https://octopus.energy/blo Export sensors are not provided as the data is not available -See [electricity entities](../entities/electricity.md#home-mini-entities) and [gas entities](../entities/gas.md#home-mini-entities) for more information. +See [electricity entities](../entities/electricity.md#home-minipro-entities) and [gas entities](../entities/gas.md#home-minipro-entities) for more information. ### Refresh Rate In Minutes diff --git a/_docs/setup/tariff_comparison.md b/_docs/setup/tariff_comparison.md index a3d1932f..83f840d2 100644 --- a/_docs/setup/tariff_comparison.md +++ b/_docs/setup/tariff_comparison.md @@ -50,9 +50,7 @@ This will display the cost of your previous accumulative consumption against the !!! info - These sensors will compare the same time period as the [electricity previous accumulative consumption](../entities/electricity.md#previous-accumulative-consumption) or [gas previous accumulative consumption](../entities/gas.md#previous-accumulative-consumption-m3). - - If you have changed the [offset](./account.md#previous-consumption-days-offset), then this sensor will use the same offset. + These sensors will compare the same time period as the [electricity previous accumulative consumption](../entities/electricity.md#previous-accumulative-consumption) or [gas previous accumulative consumption](../entities/gas.md#previous-accumulative-consumption-m3). ### Previous Consumption Override Day Rates @@ -65,9 +63,7 @@ The state of this sensor states when the previous consumption tariff comparison !!! info - These sensors will provide rates for the same time period as the [electricity previous accumulative consumption](../entities/electricity.md#previous-accumulative-consumption) or [gas previous accumulative consumption](../entities/gas.md#previous-accumulative-consumption-m3). - - If you have changed the [offset](./account.md#previous-consumption-days-offset), then this sensor will use the same offset. + These sensors will provide rates for the same time period as the [electricity previous accumulative consumption](../entities/electricity.md#previous-accumulative-consumption) or [gas previous accumulative consumption](../entities/gas.md#previous-accumulative-consumption-m3). | Attribute | Type | Description | |-----------|------|-------------| diff --git a/custom_components/octopus_energy/const.py b/custom_components/octopus_energy/const.py index b8e5b10f..844877c2 100644 --- a/custom_components/octopus_energy/const.py +++ b/custom_components/octopus_energy/const.py @@ -187,14 +187,12 @@ EVENT_ELECTRICITY_CURRENT_DAY_RATES = "octopus_energy_electricity_current_day_rates" EVENT_ELECTRICITY_NEXT_DAY_RATES = "octopus_energy_electricity_next_day_rates" EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_RATES = "octopus_energy_electricity_previous_consumption_rates" -EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_OVERRIDE_RATES = "octopus_energy_electricity_previous_consumption_override_rates" EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_TARIFF_COMPARISON_RATES = "octopus_energy_elec_previous_consumption_tariff_comparison_rates" EVENT_GAS_PREVIOUS_DAY_RATES = "octopus_energy_gas_previous_day_rates" EVENT_GAS_CURRENT_DAY_RATES = "octopus_energy_gas_current_day_rates" EVENT_GAS_NEXT_DAY_RATES = "octopus_energy_gas_next_day_rates" EVENT_GAS_PREVIOUS_CONSUMPTION_RATES = "octopus_energy_gas_previous_consumption_rates" -EVENT_GAS_PREVIOUS_CONSUMPTION_OVERRIDE_RATES = "octopus_energy_gas_previous_consumption_override_rates" EVENT_GAS_PREVIOUS_CONSUMPTION_TARIFF_COMPARISON_RATES = "octopus_energy_gas_previous_consumption_tariff_comparison_rates" EVENT_NEW_SAVING_SESSION = "octopus_energy_new_octoplus_saving_session"