diff --git a/README.md b/README.md index 902c3cd0..017fc5da 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - [Manual](#manual) - [How to setup](#how-to-setup) - [Target Rate Sensors](#target-rate-sensors) + - [Events](#events) - [Energy Dashboard](#energy-dashboard) - [Community Contributions](#community-contributions) - [FAQ](#faq) @@ -39,7 +40,7 @@ To install, place the contents of `custom_components` into the ` + New rates available for {{ trigger.event.data.mpan }}. Starting value is {{ trigger.event.data.rates[0]["value_inc_vat"] }} + target: <@ULU7111GU> + length_hint: 00:00:04 +``` + +## Electricity Current Day Rates + +`octopus_energy_electricity_current_day_rates` + +This is fired when the current day rates are updated. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the current day | +| `tariff_code` | `string` | The tariff code associated with current day's rates | +| `mpan` | `string` | The mpan of the meter associated with these rates | + +## Electricity Previous Day Rates + +`octopus_energy_electricity_previous_day_rates` + +This is fired when the previous day rates are updated. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the previous day | +| `tariff_code` | `string` | The tariff code associated with previous day's rates | +| `mpan` | `string` | The mpan of the meter associated with these rates | + +## Electricity Next Day Rates + +`octopus_energy_electricity_next_day_rates` + +This is fired when the next day rates are updated. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the next day | +| `tariff_code` | `string` | The tariff code associated with next day's rates | +| `mpan` | `string` | The mpan of the meter associated with these rates | + +## Electricity Previous Consumption Rates + +`octopus_energy_electricity_previous_consumption_rates` + +This is fired when the [previous consumption's](./sensors/electricity.md#previous-accumulative-consumption) rates are updated. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the previous consumption | +| `tariff_code` | `string` | The tariff code associated with previous consumption's rates | +| `mpan` | `string` | The mpan of the meter associated with these rates | + +## Electricity Previous Consumption Override Rates + +`octopus_energy_electricity_previous_consumption_override_rates` + +This is fired when the [previous consumption override's](./sensors/electricity.md#tariff-overrides) rates are updated. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | 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 | + +## Gas Current Day Rates + +`octopus_energy_gas_current_day_rates` + +This is fired when the current day rates are updated. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the current day | +| `tariff_code` | `string` | The tariff code associated with current day's rates | +| `mprn` | `string` | The mprn of the meter associated with these rates | + +## Gas Previous Day Rates + +`octopus_energy_gas_previous_day_rates` + +This is fired when the previous day rates are updated. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the previous day | +| `tariff_code` | `string` | The tariff code associated with previous day's rates | +| `mprn` | `string` | The mprn of the meter associated with these rates | + +## Gas Next Day Rates + +`octopus_energy_gas_next_day_rates` + +This is fired when the next day rates are updated. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the next day | +| `tariff_code` | `string` | The tariff code associated with next day's rates | +| `mprn` | `string` | The mprn of the meter associated with these rates | + +## Gas Previous Consumption Rates + +`octopus_energy_gas_previous_consumption_rates` + +This is fired when the [previous consumption's](./sensors/gas.md#previous-accumulative-consumption) rates are updated. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the previous consumption | +| `tariff_code` | `string` | The tariff code associated with previous consumption's rates | +| `mprn` | `string` | The mprn of the meter associated with these rates | + +## Gas Previous Consumption Override Rates + +`octopus_energy_gas_previous_consumption_override_rates` + +This is fired when the [previous consumption override's](./sensors/gas.md#tariff-overrides) rates are updated. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | 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 | \ No newline at end of file diff --git a/_docs/sensors/electricity.md b/_docs/sensors/electricity.md index f120305b..646e1cc2 100644 --- a/_docs/sensors/electricity.md +++ b/_docs/sensors/electricity.md @@ -6,6 +6,9 @@ You'll get the following sensors for each electricity meter with an active agree - [Current Rate](#current-rate) - [Previous Rate](#previous-rate) - [Next rate](#next-rate) + - [Current Day Rates](#current-day-rates) + - [Previous Day Rates](#previous-day-rates) + - [Next Day Rates](#next-day-rates) - [Off Peak](#off-peak) - [Smart Meter Sensors](#smart-meter-sensors) - [Previous Accumulative Consumption](#previous-accumulative-consumption) @@ -14,6 +17,7 @@ You'll get the following sensors for each electricity meter with an active agree - [Previous Accumulative Cost](#previous-accumulative-cost) - [Previous Accumulative Cost (Peak Rate)](#previous-accumulative-cost-peak-rate) - [Previous Accumulative Cost (Off Peak Rate)](#previous-accumulative-cost-off-peak-rate) + - [Previous Consumption Day Rates](#previous-consumption-day-rates) - [Export Sensors](#export-sensors) - [Home Mini Sensors](#home-mini-sensors) - [Current Consumption](#current-consumption) @@ -25,9 +29,10 @@ You'll get the following sensors for each electricity meter with an active agree - [Current Accumulative Cost (Peak Rate)](#current-accumulative-cost-peak-rate) - [Current Accumulative Cost (Off Peak Rate)](#current-accumulative-cost-off-peak-rate) - [Tariff Overrides](#tariff-overrides) - - [Previous Accumulative Cost Override Tariff (Electricity)](#previous-accumulative-cost-override-tariff-electricity) + - [Previous Accumulative Cost Override Tariff](#previous-accumulative-cost-override-tariff) - [How To Use](#how-to-use) - - [Previous Accumulative Cost Override (Electricity)](#previous-accumulative-cost-override-electricity) + - [Previous Accumulative Cost Override](#previous-accumulative-cost-override) + - [Previous Consumption Override Day Rates](#previous-consumption-override-day-rates) ## Current Rate @@ -85,6 +90,39 @@ The next/upcoming rate that energy consumption will be charged at (including VAT | `valid_from` | `datetime` | The date/time when the rate is valid from | | `valid_to` | `datetime` | The date/time when the rate is valid to | +## Current Day Rates + +`event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_current_day_rates` + +The state of this sensor states when the current day's rates were last updated. The attributes of this sensor exposes the current day's rates. This is disabled by default. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the current day | +| `tariff_code` | `string` | The tariff code associated with current day's rates | + +## Previous Day Rates + +`event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_previous_day_rates` + +The state of this sensor states when the previous day's rates were last updated. The attributes of this sensor exposes the previous day's rates. This is disabled by default. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the previous day | +| `tariff_code` | `string` | The tariff code associated with previous day's rates | + +## Next Day Rates + +`event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_next_day_rates` + +The state of this sensor states when the next day's rates were last updated. The attributes of this sensor exposes the next day's rates. This is disabled by default. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the next day | +| `tariff_code` | `string` | The tariff code associated with today's rates | + ## Off Peak `binary_sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_off_peak` @@ -185,6 +223,17 @@ The total cost for the previous day that applied during off peak hours. This is | `is_export` | `boolean` | Determines if the meter exports energy rather than imports | | `is_smart_meter` | `boolean` | Determines if the meter is considered smart by Octopus Energy | +## Previous Consumption Day Rates + +`event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_previous_consumption_rates` + +The state of this sensor states when the previous consumption's rates were last updated. This is typically the same as the previous day's rates, but could differ if the default offset is changed. The attributes of this sensor exposes the previous consumption's rates. This is disabled by default. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the previous consumption | +| `tariff_code` | `string` | The tariff code associated with previous consumption's rates | + ## Export Sensors If you export energy, then in addition you'll gain the above sensors with the name `export` present. E.g. `sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_export_current_rate`. @@ -313,7 +362,7 @@ See [below](#previous-accumulative-cost-override-tariff-electricity) for instruc > Please note: When updating the tariff depending on what previous consumption data is available, it can take up to 24 hours to update the cost. This will be improved in the future. -### Previous Accumulative Cost Override Tariff (Electricity) +### Previous Accumulative Cost Override Tariff `text.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_previous_accumulative_cost_override_tariff` @@ -330,10 +379,21 @@ Once you have found your target tariff > Please note: When updating the tariff depending on what previous consumption data is available, it can take up to 24 hours to update the cost. This will be improved in the future. -### Previous Accumulative Cost Override (Electricity) +### Previous Accumulative Cost Override `sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_previous_accumulative_cost_override` This is the cost of the previous electricity accumulation based on the specified tariff override. For attributes, see [Previous Accumulative Cost](#previous-accumulative-cost). + +## Previous Consumption Override Day Rates + +`event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_previous_consumption_override_rates` + +The state of this sensor states when the previous consumption override's rates were last updated. The attributes of this sensor exposes the previous consumption override's rates. This is disabled by default. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the previous consumption override | +| `tariff_code` | `string` | The tariff code associated with previous consumption override's rates | \ No newline at end of file diff --git a/_docs/sensors/gas.md b/_docs/sensors/gas.md index dc1ac3f8..9b3f9de7 100644 --- a/_docs/sensors/gas.md +++ b/_docs/sensors/gas.md @@ -6,17 +6,23 @@ You'll get the following sensors for each gas meter with an active agreement: - [Current Rate](#current-rate) - [Previous Rate](#previous-rate) - [Next rate](#next-rate) + - [Current Day Rates](#current-day-rates) + - [Previous Day Rates](#previous-day-rates) + - [Next Day Rates](#next-day-rates) - [Smart Meter Sensors](#smart-meter-sensors) - [Previous Accumulative Consumption](#previous-accumulative-consumption) - [Previous Accumulative Consumption (kWH)](#previous-accumulative-consumption-kwh) - [Previous Accumulative Cost](#previous-accumulative-cost) + - [Previous Consumption Day Rates](#previous-consumption-day-rates) - [Home Mini Sensors](#home-mini-sensors) - [Current Consumption](#current-consumption) - [Current Accumulative Consumption](#current-accumulative-consumption) - [Current Accumulative Cost](#current-accumulative-cost) - [Tariff Overrides](#tariff-overrides) - - [Previous Accumulative Cost Override Tariff (Gas)](#previous-accumulative-cost-override-tariff-gas) - - [Previous Accumulative Cost Override (Gas)](#previous-accumulative-cost-override-gas) + - [Previous Accumulative Cost Override Tariff](#previous-accumulative-cost-override-tariff) + - [How To Use](#how-to-use) + - [Previous Accumulative Cost Override](#previous-accumulative-cost-override) + - [Previous Consumption Override Day Rates](#previous-consumption-override-day-rates) ## Current Rate @@ -67,6 +73,39 @@ The next/upcoming rate that energy consumption will be charged at (including VAT | `valid_from` | `datetime` | The date/time when the rate is valid from | | `valid_to` | `datetime` | The date/time when the rate is valid to | +## Current Day Rates + +`event.octopus_energy_gas_{{METER_SERIAL_NUMBER}}_{{MPRN_NUMBER}}_current_day_rates` + +The state of this sensor states when the current day's rates were last updated. The attributes of this sensor exposes the current day's rates. This is disabled by default. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the current day | +| `tariff_code` | `string` | The tariff code associated with current day's rates | + +## Previous Day Rates + +`event.octopus_energy_gas_{{METER_SERIAL_NUMBER}}_{{MPRN_NUMBER}}_previous_day_rates` + +The state of this sensor states when the previous day's rates were last updated. The attributes of this sensor exposes the previous day's rates. This is disabled by default. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the previous day | +| `tariff_code` | `string` | The tariff code associated with previous day's rates | + +## Next Day Rates + +`event.octopus_energy_gas_{{METER_SERIAL_NUMBER}}_{{MPRN_NUMBER}}_next_day_rates` + +The state of this sensor states when the next day's rates were last updated. The attributes of this sensor exposes the next day's rates. This is disabled by default. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the next day | +| `tariff_code` | `string` | The tariff code associated with today's rates | + ## Smart Meter Sensors If your account information doesn't determine you have a smart meter, then you will have the following sensors in a disabled state. If you enable these sensors, they might not work correctly in this scenario. @@ -131,6 +170,17 @@ The total cost for the previous day, including the standing charge. | `last_calculated_timestamp` | `datetime` | The timestamp determining when the cost was last calculated. | | `calorific_value` | `float` | The calorific value used for the calculations, as set in your [account](../setup_account.md#calorific-value). | +## Previous Consumption Day Rates + +`event.octopus_energy_gas_{{METER_SERIAL_NUMBER}}_{{MPRN_NUMBER}}_previous_consumption_rates` + +The state of this sensor states when the previous consumption's rates were last updated. This is typically the same as the previous day's rates, but could differ if the default offset is changed. The attributes of this sensor exposes the previous consumption's rates. This is disabled by default. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the previous consumption | +| `tariff_code` | `string` | The tariff code associated with previous consumption's rates | + ## Home Mini Sensors ### Current Consumption @@ -188,23 +238,38 @@ Instructions on how to find tariffs can be found in the [faq](../faq.md#i-want-t > Please note: When updating the tariff depending on what previous consumption data is available, it can take up to 24 hours to update the cost. This will be improved in the future. -Once enabled, you can set the tariff you wish to use for the override in the device controls - -1. Navigate to [your devices](https://my.home-assistant.io/redirect/devices/) -2. Search for "Octopus Energy" -3. Click on one of the meters -4. Enter the tariff code in the Controls field for the override sensor. - -### Previous Accumulative Cost Override Tariff (Gas) +### Previous Accumulative Cost Override Tariff `text.octopus_energy_gas_{{METER_SERIAL_NUMBER}}_{{MPRN_NUMBER}}_previous_accumulative_cost_override_tariff` This is used to define the gas tariff you want to compare -### Previous Accumulative Cost Override (Gas) +#### How To Use + +Instructions on how to find tariffs can be found in the [faq](../faq.md#i-want-to-use-the-tariff-overrides-but-how-do-i-find-an-available-tariff). + +Once you have found your target tariff + +1. Click on this entity to open the info dialog. +2. Enter your tariff in the text box, and hit `enter` on your keyboard to confirm + +> Please note: When updating the tariff depending on what previous consumption data is available, it can take up to 24 hours to update the cost. This will be improved in the future. + +### Previous Accumulative Cost Override `sensor.octopus_energy_gas_{{METER_SERIAL_NUMBER}}_{{MPRN_NUMBER}}_previous_accumulative_cost_override` This is the cost of the previous gas accumulation based on the specified tariff override. -For attributes, see [Previous Accumulative Cost](#previous-accumulative-cost). \ No newline at end of file +For attributes, see [Previous Accumulative Cost](#previous-accumulative-cost). + +## Previous Consumption Override Day Rates + +`event.octopus_energy_gas_{{METER_SERIAL_NUMBER}}_{{MPRN_NUMBER}}_previous_consumption_override_rates` + +The state of this sensor states when the previous consumption override's rates were last updated. The attributes of this sensor exposes the previous consumption override's rates. This is disabled by default. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `rates` | `list` | The list of rates applicable for the previous consumption override | +| `tariff_code` | `string` | The tariff code associated with previous consumption override's rates | \ No newline at end of file diff --git a/custom_components/octopus_energy/__init__.py b/custom_components/octopus_energy/__init__.py index 37b8e159..c2d068d4 100644 --- a/custom_components/octopus_energy/__init__.py +++ b/custom_components/octopus_energy/__init__.py @@ -100,6 +100,10 @@ async def async_setup_entry(hass, entry): hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "time") ) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "event") + ) elif CONFIG_TARGET_NAME in config: if DOMAIN not in hass.data or DATA_ELECTRICITY_RATES_COORDINATOR not in hass.data[DOMAIN] or DATA_ACCOUNT not in hass.data[DOMAIN]: raise ConfigEntryNotReady("Electricity rates have not been setup") diff --git a/custom_components/octopus_energy/const.py b/custom_components/octopus_energy/const.py index bca64c45..47551572 100644 --- a/custom_components/octopus_energy/const.py +++ b/custom_components/octopus_energy/const.py @@ -81,3 +81,18 @@ vol.Optional(CONFIG_MAIN_ELECTRICITY_PRICE_CAP): cv.positive_float, vol.Optional(CONFIG_MAIN_GAS_PRICE_CAP): cv.positive_float }) + +EVENT_ELECTRICITY_PREVIOUS_DAY_RATES = "octopus_energy_electricity_previous_day_rates" +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_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" + +# During BST, two records are returned before the rest of the data is available +MINIMUM_CONSUMPTION_DATA_LENGTH = 3 diff --git a/custom_components/octopus_energy/coordinators/__init__.py b/custom_components/octopus_energy/coordinators/__init__.py index edd2347b..81149c60 100644 --- a/custom_components/octopus_energy/coordinators/__init__.py +++ b/custom_components/octopus_energy/coordinators/__init__.py @@ -1,13 +1,12 @@ -from datetime import datetime +from datetime import datetime, timedelta import logging +from typing import Callable, Any from homeassistant.helpers import issue_registry as ir - -from homeassistant.util.dt import (now) +from homeassistant.util.dt import (as_utc) from ..const import ( DOMAIN, - DATA_ACCOUNT, ) from ..api_client import OctopusEnergyApiClient @@ -89,4 +88,39 @@ def get_current_gas_agreement_tariff_codes(current: datetime, account_info): if key not in tariff_codes: tariff_codes[(point["mprn"], is_smart_meter)] = active_tariff_code - return tariff_codes \ No newline at end of file + return tariff_codes + +def raise_rate_events(now: datetime, + rates: list, + additional_attributes: "dict[str, Any]", + fire_event: Callable[[str, "dict[str, Any]"], None], + previous_event_key: str, + current_event_key: str, + next_event_key: str): + + today_start = as_utc(now.replace(hour=0, minute=0, second=0, microsecond=0)) + today_end = today_start + timedelta(days=1) + + previous_rates = [] + current_rates = [] + next_rates = [] + + for rate in rates: + if (rate["valid_from"] < today_start): + previous_rates.append(rate) + elif (rate["valid_from"] >= today_end): + next_rates.append(rate) + else: + current_rates.append(rate) + + event_data = { "rates": previous_rates } + event_data.update(additional_attributes) + fire_event(previous_event_key, event_data) + + event_data = { "rates": current_rates } + event_data.update(additional_attributes) + fire_event(current_event_key, event_data) + + event_data = { "rates": next_rates } + event_data.update(additional_attributes) + fire_event(next_event_key, event_data) \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/electricity_rates.py b/custom_components/octopus_energy/coordinators/electricity_rates.py index c79b99e3..eeb55b5b 100644 --- a/custom_components/octopus_energy/coordinators/electricity_rates.py +++ b/custom_components/octopus_energy/coordinators/electricity_rates.py @@ -1,5 +1,6 @@ import logging from datetime import datetime, timedelta +from typing import Callable, Any from homeassistant.util.dt import (now, as_utc) from homeassistant.helpers.update_coordinator import ( @@ -13,11 +14,14 @@ DATA_ELECTRICITY_RATES, DATA_ACCOUNT, DATA_INTELLIGENT_DISPATCHES, + EVENT_ELECTRICITY_CURRENT_DAY_RATES, + EVENT_ELECTRICITY_NEXT_DAY_RATES, + EVENT_ELECTRICITY_PREVIOUS_DAY_RATES, ) from ..api_client import OctopusEnergyApiClient -from . import get_current_electricity_agreement_tariff_codes +from . import get_current_electricity_agreement_tariff_codes, raise_rate_events from ..intelligent import adjust_intelligent_rates _LOGGER = logging.getLogger(__name__) @@ -27,7 +31,8 @@ async def async_refresh_electricity_rates_data( client: OctopusEnergyApiClient, account_info, existing_rates: list, - dispatches: list + dispatches: list, + fire_event: Callable[[str, "dict[str, Any]"], None], ): if (account_info is not None): tariff_codes = get_current_electricity_agreement_tariff_codes(current, account_info) @@ -49,8 +54,6 @@ async def async_refresh_electricity_rates_data( _LOGGER.debug(f'Electricity rates retrieved for {tariff_code}') except: _LOGGER.debug('Failed to retrieve electricity rates') - else: - new_rates = existing_rates[key] if new_rates is not None: if dispatches is not None: @@ -61,6 +64,15 @@ async def async_refresh_electricity_rates_data( _LOGGER.debug(f"Rates adjusted: {rates[key]}; dispatches: {dispatches}") else: rates[key] = new_rates + + raise_rate_events(current, + rates[key], + { "mpan": meter_point, "tariff_code": tariff_code }, + fire_event, + EVENT_ELECTRICITY_PREVIOUS_DAY_RATES, + EVENT_ELECTRICITY_CURRENT_DAY_RATES, + EVENT_ELECTRICITY_NEXT_DAY_RATES) + elif (existing_rates is not None and key in existing_rates): _LOGGER.debug(f"Failed to retrieve new electricity rates for {tariff_code}, so using cached rates") rates[key] = existing_rates[key] @@ -86,7 +98,8 @@ async def async_update_electricity_rates_data(): client, account_info, rates, - dispatches + dispatches, + hass.bus.async_fire ) return hass.data[DOMAIN][DATA_ELECTRICITY_RATES] diff --git a/custom_components/octopus_energy/coordinators/gas_rates.py b/custom_components/octopus_energy/coordinators/gas_rates.py index d978c82f..059220c9 100644 --- a/custom_components/octopus_energy/coordinators/gas_rates.py +++ b/custom_components/octopus_energy/coordinators/gas_rates.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta import logging +from typing import Callable, Any from homeassistant.util.dt import (now, as_utc) from homeassistant.helpers.update_coordinator import ( @@ -9,12 +10,15 @@ from ..const import ( DATA_ACCOUNT, DOMAIN, - DATA_GAS_RATES + DATA_GAS_RATES, + EVENT_GAS_CURRENT_DAY_RATES, + EVENT_GAS_NEXT_DAY_RATES, + EVENT_GAS_PREVIOUS_DAY_RATES ) from ..api_client import (OctopusEnergyApiClient) -from . import get_current_gas_agreement_tariff_codes +from . import get_current_gas_agreement_tariff_codes, raise_rate_events _LOGGER = logging.getLogger(__name__) @@ -22,7 +26,8 @@ async def async_refresh_gas_rates_data( current: datetime, client: OctopusEnergyApiClient, account_info, - existing_rates: list + existing_rates: list, + fire_event: Callable[[str, "dict[str, Any]"], None], ): if (account_info is not None): tariff_codes = get_current_gas_agreement_tariff_codes(current, account_info) @@ -44,11 +49,17 @@ async def async_refresh_gas_rates_data( _LOGGER.debug(f'Gas rates retrieved for {tariff_code}') except: _LOGGER.debug('Failed to retrieve gas rates') - else: - new_rates = existing_rates[key] if new_rates is not None: rates[key] = new_rates + + raise_rate_events(current, + rates[key], + { "mprn": meter_point, "tariff_code": tariff_code }, + fire_event, + EVENT_GAS_PREVIOUS_DAY_RATES, + EVENT_GAS_CURRENT_DAY_RATES, + EVENT_GAS_NEXT_DAY_RATES) elif (existing_rates is not None and key in existing_rates): _LOGGER.debug(f"Failed to retrieve new gas rates for {tariff_code}, so using cached rates") rates[key] = existing_rates[key] @@ -72,7 +83,8 @@ async def async_update_data(): current, client, account_info, - rates + rates, + hass.bus.async_fire ) return hass.data[DOMAIN][DATA_GAS_RATES] diff --git a/custom_components/octopus_energy/coordinators/previous_consumption_and_rates.py b/custom_components/octopus_energy/coordinators/previous_consumption_and_rates.py index 977e68d2..fd5e337d 100644 --- a/custom_components/octopus_energy/coordinators/previous_consumption_and_rates.py +++ b/custom_components/octopus_energy/coordinators/previous_consumption_and_rates.py @@ -1,5 +1,6 @@ from datetime import timedelta import logging +from typing import Callable, Any from homeassistant.util.dt import (utcnow, now, as_utc) from homeassistant.helpers.update_coordinator import ( @@ -8,7 +9,10 @@ from ..const import ( DOMAIN, - DATA_INTELLIGENT_DISPATCHES + DATA_INTELLIGENT_DISPATCHES, + EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_RATES, + EVENT_GAS_PREVIOUS_CONSUMPTION_RATES, + MINIMUM_CONSUMPTION_DATA_LENGTH ) from ..api_client import (OctopusEnergyApiClient) @@ -36,6 +40,7 @@ async def async_fetch_consumption_and_rates( is_electricity: bool, tariff_code: str, is_smart_meter: bool, + fire_event: Callable[[str, "dict[str, Any]"], None], intelligent_dispatches = None ): @@ -46,6 +51,8 @@ async def async_fetch_consumption_and_rates( previous_data["consumption"][-1]["interval_end"] < period_to) and utc_now.minute % 30 == 0)): + _LOGGER.debug(f"Retrieving previous consumption data for {'electricity' if is_electricity else 'gas'} {identifier}/{serial_number}...") + try: if (is_electricity == True): consumption_data = await client.async_get_electricity_consumption(identifier, serial_number, period_from, period_to) @@ -57,25 +64,31 @@ async def async_fetch_consumption_and_rates( _LOGGER.debug(f"Tariff: {tariff_code}; dispatches: {intelligent_dispatches}") standing_charge = await client.async_get_electricity_standing_charge(tariff_code, period_from, period_to) - - _LOGGER.debug(f'Previous Electricity consumption, rates and standing charges retrieved for {tariff_code}') else: consumption_data = await client.async_get_gas_consumption(identifier, serial_number, period_from, period_to) rate_data = await client.async_get_gas_rates(tariff_code, period_from, period_to) standing_charge = await client.async_get_gas_standing_charge(tariff_code, period_from, period_to) - - _LOGGER.debug(f'Previous Gas consumption, rates and standing charges retrieved for {tariff_code}') - if consumption_data is not None and len(consumption_data) > 0 and rate_data is not None and len(rate_data) > 0 and standing_charge is not None: + if consumption_data is not None and len(consumption_data) >= MINIMUM_CONSUMPTION_DATA_LENGTH and rate_data is not None and len(rate_data) > 0 and standing_charge is not None: + _LOGGER.debug(f"Discovered previous consumption data for {'electricity' if is_electricity else 'gas'} {identifier}/{serial_number}") consumption_data = __sort_consumption(consumption_data) + if (is_electricity == True): + fire_event(EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_RATES, { "mpan": identifier, "tariff_code": tariff_code, "rates": rate_data }) + else: + fire_event(EVENT_GAS_PREVIOUS_CONSUMPTION_RATES, { "mprn": identifier, "tariff_code": tariff_code, "rates": rate_data }) + + _LOGGER.debug(f"Fired event for {'electricity' if is_electricity else 'gas'} {identifier}/{serial_number}") + return { "consumption": consumption_data, "rates": rate_data, "standing_charge": standing_charge["value_inc_vat"] } + else: + _LOGGER.debug(f"Failed to retrieve previous consumption data for {'electricity' if is_electricity else 'gas'} {identifier}/{serial_number}; consumptions: {len(consumption_data)}; rates: {len(rate_data)}; standing_charge: {standing_charge is not None};") except: - _LOGGER.debug(f"Failed to retrieve {'electricity' if is_electricity else 'gas'} previous consumption and rate data") + _LOGGER.debug(f"Failed to retrieve previous consumption data for {'electricity' if is_electricity else 'gas'} {identifier}/{serial_number}") return previous_data @@ -112,6 +125,7 @@ async def async_update_data(): is_electricity, tariff_code, is_smart_meter, + hass.bus.async_fire, hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES] if DATA_INTELLIGENT_DISPATCHES in hass.data[DOMAIN] else None ) diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py b/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py index 97d8373b..ac7b9eeb 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py @@ -112,9 +112,7 @@ async def async_update(self): rate_data, standing_charge, self._last_reset, - self._tariff_code, - # During BST, two records are returned before the rest of the data is available - 3 + self._tariff_code ) if (consumption_and_cost is not None): diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_consumption_off_peak.py b/custom_components/octopus_energy/electricity/previous_accumulative_consumption_off_peak.py index b4063645..c5e10323 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_consumption_off_peak.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_consumption_off_peak.py @@ -96,9 +96,7 @@ def state(self): rate_data, standing_charge, self._last_reset, - self._tariff_code, - # During BST, two records are returned before the rest of the data is available - 3 + self._tariff_code ) if (consumption_and_cost is not None): diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_consumption_peak.py b/custom_components/octopus_energy/electricity/previous_accumulative_consumption_peak.py index 45a1d5ef..8fd0c69d 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_consumption_peak.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_consumption_peak.py @@ -96,9 +96,7 @@ def state(self): rate_data, standing_charge, self._last_reset, - self._tariff_code, - # During BST, two records are returned before the rest of the data is available - 3 + self._tariff_code ) if (consumption_and_cost is not None): diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_cost.py b/custom_components/octopus_energy/electricity/previous_accumulative_cost.py index eebf6eb1..13380d25 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_cost.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost.py @@ -107,9 +107,7 @@ async def async_update(self): rate_data, standing_charge, self._last_reset, - self._tariff_code, - # During BST, two records are returned before the rest of the data is available - 3 + self._tariff_code ) if (consumption_and_cost is not None): diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_cost_off_peak.py b/custom_components/octopus_energy/electricity/previous_accumulative_cost_off_peak.py index 21a4a088..1e0270bb 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_cost_off_peak.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_off_peak.py @@ -93,9 +93,7 @@ def state(self): rate_data, standing_charge, self._last_reset, - self._tariff_code, - # During BST, two records are returned before the rest of the data is available - 3 + self._tariff_code ) if (consumption_and_cost is not None): diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_cost_override.py b/custom_components/octopus_energy/electricity/previous_accumulative_cost_override.py index d06aaca4..6be3598c 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_cost_override.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_override.py @@ -20,7 +20,7 @@ from ..api_client import (OctopusEnergyApiClient) -from ..const import (DOMAIN) +from ..const import (DOMAIN, EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_OVERRIDE_RATES, MINIMUM_CONSUMPTION_DATA_LENGTH) from . import get_electricity_tariff_override_key @@ -112,7 +112,7 @@ async def async_update(self): is_tariff_present = tariff_override_key in self._hass.data[DOMAIN] has_tariff_changed = is_tariff_present and self._hass.data[DOMAIN][tariff_override_key] != self._tariff_code - if (consumption_data is not None and len(consumption_data) > 0 and is_tariff_present and (is_old_data or has_tariff_changed)): + if (consumption_data is not None and len(consumption_data) >= MINIMUM_CONSUMPTION_DATA_LENGTH and is_tariff_present and (is_old_data or has_tariff_changed)): _LOGGER.debug(f"Calculating previous electricity consumption cost override for '{self._mpan}/{self._serial_number}'...") tariff_override = self._hass.data[DOMAIN][tariff_override_key] @@ -126,9 +126,7 @@ async def async_update(self): rate_data, standing_charge["value_inc_vat"] if standing_charge is not None else None, None if has_tariff_changed else self._last_reset, - tariff_override, - # During BST, two records are returned before the rest of the data is available - 3 + tariff_override ) self._tariff_code = tariff_override @@ -158,6 +156,8 @@ async def async_update(self): }, consumption_and_cost["charges"])) } + self._hass.bus.async_fire(EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_OVERRIDE_RATES, { "mpan": self._mpan, "tariff_code": self._tariff_code, "rates": rate_data }) + async def async_added_to_hass(self): """Call when entity about to be added to hass.""" # If not None, we got an initial value. diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_cost_peak.py b/custom_components/octopus_energy/electricity/previous_accumulative_cost_peak.py index 17b79155..4d1966e9 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_cost_peak.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_peak.py @@ -93,9 +93,7 @@ def state(self): rate_data, standing_charge, self._last_reset, - self._tariff_code, - # During BST, two records are returned before the rest of the data is available - 3 + self._tariff_code ) if (consumption_and_cost is not None): diff --git a/custom_components/octopus_energy/electricity/rates_current_day.py b/custom_components/octopus_energy/electricity/rates_current_day.py new file mode 100644 index 00000000..a7d288d6 --- /dev/null +++ b/custom_components/octopus_energy/electricity/rates_current_day.py @@ -0,0 +1,69 @@ +import logging + +from homeassistant.core import HomeAssistant, callback + +from homeassistant.components.event import ( + EventEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from .base import (OctopusEnergyElectricitySensor) +from ..const import EVENT_ELECTRICITY_CURRENT_DAY_RATES + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityCurrentDayRates(OctopusEnergyElectricitySensor, EventEntity, RestoreEntity): + """Sensor for displaying the current day's rates.""" + + def __init__(self, hass: HomeAssistant, meter, point): + """Init sensor.""" + # Pass coordinator to base class + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._hass = hass + self._state = None + self._last_updated = None + + self._attr_event_types = [EVENT_ELECTRICITY_CURRENT_DAY_RATES] + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_current_day_rates" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Current Day Rates" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyElectricityCurrentDayRates state: {self._state}') + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self._hass.bus.async_listen(self._attr_event_types[0], self._async_handle_event) + + @callback + def _async_handle_event(self, event) -> None: + if (event.data is not None and "mpan" in event.data and event.data["mpan"] == self._mpan): + self._trigger_event(event.event_type, event.data) + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/rates_next_day.py b/custom_components/octopus_energy/electricity/rates_next_day.py new file mode 100644 index 00000000..65f721b7 --- /dev/null +++ b/custom_components/octopus_energy/electricity/rates_next_day.py @@ -0,0 +1,69 @@ +import logging + +from homeassistant.core import HomeAssistant, callback + +from homeassistant.components.event import ( + EventEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from .base import (OctopusEnergyElectricitySensor) +from ..const import EVENT_ELECTRICITY_NEXT_DAY_RATES + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityNextDayRates(OctopusEnergyElectricitySensor, EventEntity, RestoreEntity): + """Sensor for displaying the next day's rates.""" + + def __init__(self, hass: HomeAssistant, meter, point): + """Init sensor.""" + # Pass coordinator to base class + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._hass = hass + self._state = None + self._last_updated = None + + self._attr_event_types = [EVENT_ELECTRICITY_NEXT_DAY_RATES] + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_next_day_rates" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Next Day Rates" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyElectricityNextDayRates state: {self._state}') + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self._hass.bus.async_listen(self._attr_event_types[0], self._async_handle_event) + + @callback + def _async_handle_event(self, event) -> None: + if (event.data is not None and "mpan" in event.data and event.data["mpan"] == self._mpan): + self._trigger_event(event.event_type, event.data) + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/rates_previous_consumption.py b/custom_components/octopus_energy/electricity/rates_previous_consumption.py new file mode 100644 index 00000000..153a637f --- /dev/null +++ b/custom_components/octopus_energy/electricity/rates_previous_consumption.py @@ -0,0 +1,69 @@ +import logging + +from homeassistant.core import HomeAssistant, callback + +from homeassistant.components.event import ( + EventEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from .base import (OctopusEnergyElectricitySensor) +from ..const import EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_RATES + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityPreviousConsumptionRates(OctopusEnergyElectricitySensor, EventEntity, RestoreEntity): + """Sensor for displaying the previous consumption's rates.""" + + def __init__(self, hass: HomeAssistant, meter, point): + """Init sensor.""" + # Pass coordinator to base class + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._hass = hass + self._state = None + self._last_updated = None + + self._attr_event_types = [EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_RATES] + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_consumption_rates" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Consumption Rates" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyElectricityPreviousConsumptionRates state: {self._state}') + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self._hass.bus.async_listen(self._attr_event_types[0], self._async_handle_event) + + @callback + def _async_handle_event(self, event) -> None: + if (event.data is not None and "mpan" in event.data and event.data["mpan"] == self._mpan): + self._trigger_event(event.event_type, event.data) + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/rates_previous_consumption_override.py b/custom_components/octopus_energy/electricity/rates_previous_consumption_override.py new file mode 100644 index 00000000..191057db --- /dev/null +++ b/custom_components/octopus_energy/electricity/rates_previous_consumption_override.py @@ -0,0 +1,69 @@ +import logging + +from homeassistant.core import HomeAssistant, callback + +from homeassistant.components.event import ( + EventEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from .base import (OctopusEnergyElectricitySensor) +from ..const import EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_OVERRIDE_RATES + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityPreviousConsumptionOverrideRates(OctopusEnergyElectricitySensor, EventEntity, RestoreEntity): + """Sensor for displaying the previous consumption override's rates.""" + + def __init__(self, hass: HomeAssistant, meter, point): + """Init sensor.""" + # Pass coordinator to base class + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._hass = hass + self._state = None + self._last_updated = None + + self._attr_event_types = [EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_OVERRIDE_RATES] + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_consumption_override_rates" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Consumption Override Rates" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyElectricityPreviousConsumptionOverrideRates state: {self._state}') + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self._hass.bus.async_listen(self._attr_event_types[0], self._async_handle_event) + + @callback + def _async_handle_event(self, event) -> None: + if (event.data is not None and "mpan" in event.data and event.data["mpan"] == self._mpan): + self._trigger_event(event.event_type, event.data) + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/rates_previous_day.py b/custom_components/octopus_energy/electricity/rates_previous_day.py new file mode 100644 index 00000000..f19f8e70 --- /dev/null +++ b/custom_components/octopus_energy/electricity/rates_previous_day.py @@ -0,0 +1,69 @@ +import logging + +from homeassistant.core import HomeAssistant, callback + +from homeassistant.components.event import ( + EventEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from .base import (OctopusEnergyElectricitySensor) +from ..const import EVENT_ELECTRICITY_PREVIOUS_DAY_RATES + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyElectricityPreviousDayRates(OctopusEnergyElectricitySensor, EventEntity, RestoreEntity): + """Sensor for displaying the previous day's rates.""" + + def __init__(self, hass: HomeAssistant, meter, point): + """Init sensor.""" + # Pass coordinator to base class + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + + self._hass = hass + self._state = None + self._last_updated = None + + self._attr_event_types = [EVENT_ELECTRICITY_PREVIOUS_DAY_RATES] + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_day_rates" + + @property + def name(self): + """Name of the sensor.""" + return f"Electricity {self._serial_number} {self._mpan}{self._export_name_addition} Previous Day Rates" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyElectricityPreviousDayRates state: {self._state}') + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self._hass.bus.async_listen(self._attr_event_types[0], self._async_handle_event) + + @callback + def _async_handle_event(self, event) -> None: + if (event.data is not None and "mpan" in event.data and event.data["mpan"] == self._mpan): + self._trigger_event(event.event_type, event.data) + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/event.py b/custom_components/octopus_energy/event.py new file mode 100644 index 00000000..6b770f2b --- /dev/null +++ b/custom_components/octopus_energy/event.py @@ -0,0 +1,70 @@ +import logging + +from homeassistant.util.dt import (utcnow) + +from .utils import get_active_tariff_code +from .electricity.rates_previous_day import OctopusEnergyElectricityPreviousDayRates +from .electricity.rates_current_day import OctopusEnergyElectricityCurrentDayRates +from .electricity.rates_next_day import OctopusEnergyElectricityNextDayRates +from .electricity.rates_previous_consumption import OctopusEnergyElectricityPreviousConsumptionRates +from .electricity.rates_previous_consumption_override import OctopusEnergyElectricityPreviousConsumptionOverrideRates +from .gas.rates_current_day import OctopusEnergyGasCurrentDayRates +from .gas.rates_next_day import OctopusEnergyGasNextDayRates +from .gas.rates_previous_day import OctopusEnergyGasPreviousDayRates +from .gas.rates_previous_consumption import OctopusEnergyGasPreviousConsumptionRates +from .gas.rates_previous_consumption_override import OctopusEnergyGasPreviousConsumptionOverrideRates + +from .const import ( + DOMAIN, + + CONFIG_MAIN_API_KEY, + DATA_ACCOUNT +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, entry, async_add_entities): + """Setup sensors based on our entry""" + + if CONFIG_MAIN_API_KEY in entry.data: + await async_setup_main_sensors(hass, entry, async_add_entities) + + return True + +async def async_setup_main_sensors(hass, entry, async_add_entities): + _LOGGER.debug('Setting up main sensors') + config = dict(entry.data) + + if entry.options: + config.update(entry.options) + + account_info = hass.data[DOMAIN][DATA_ACCOUNT] + + now = utcnow() + entities = [] + if len(account_info["electricity_meter_points"]) > 0: + for point in account_info["electricity_meter_points"]: + # We only care about points that have active agreements + tariff_code = get_active_tariff_code(now, point["agreements"]) + if tariff_code is not None: + for meter in point["meters"]: + entities.append(OctopusEnergyElectricityPreviousDayRates(hass, meter, point)) + entities.append(OctopusEnergyElectricityCurrentDayRates(hass, meter, point)) + entities.append(OctopusEnergyElectricityNextDayRates(hass, meter, point)) + entities.append(OctopusEnergyElectricityPreviousConsumptionRates(hass, meter, point)) + entities.append(OctopusEnergyElectricityPreviousConsumptionOverrideRates(hass, meter, point)) + + if len(account_info["gas_meter_points"]) > 0: + for point in account_info["gas_meter_points"]: + # We only care about points that have active agreements + tariff_code = get_active_tariff_code(now, point["agreements"]) + if tariff_code is not None: + for meter in point["meters"]: + entities.append(OctopusEnergyGasPreviousDayRates(hass, meter, point)) + entities.append(OctopusEnergyGasCurrentDayRates(hass, meter, point)) + entities.append(OctopusEnergyGasNextDayRates(hass, meter, point)) + entities.append(OctopusEnergyGasPreviousConsumptionRates(hass, meter, point)) + entities.append(OctopusEnergyGasPreviousConsumptionOverrideRates(hass, meter, point)) + + if len(entities) > 0: + async_add_entities(entities, True) diff --git a/custom_components/octopus_energy/gas/__init__.py b/custom_components/octopus_energy/gas/__init__.py index 8c5ecef9..05b608a5 100644 --- a/custom_components/octopus_energy/gas/__init__.py +++ b/custom_components/octopus_energy/gas/__init__.py @@ -25,10 +25,9 @@ def calculate_gas_consumption_and_cost( last_reset, tariff_code, consumption_units, - calorific_value, - minimum_consumption_records = 0 + calorific_value ): - if (consumption_data is not None and len(consumption_data) >= minimum_consumption_records and rate_data is not None and len(rate_data) > 0 and standing_charge is not None): + if (consumption_data is not None and len(consumption_data) > 0 and rate_data is not None and len(rate_data) > 0 and standing_charge is not None): sorted_consumption_data = __sort_consumption(consumption_data) diff --git a/custom_components/octopus_energy/gas/previous_accumulative_consumption.py b/custom_components/octopus_energy/gas/previous_accumulative_consumption.py index eb6f56c7..5b8f0eb1 100644 --- a/custom_components/octopus_energy/gas/previous_accumulative_consumption.py +++ b/custom_components/octopus_energy/gas/previous_accumulative_consumption.py @@ -116,9 +116,7 @@ async def async_update(self): self._last_reset, self._tariff_code, self._native_consumption_units, - self._calorific_value, - # During BST, two records are returned before the rest of the data is available - 3 + self._calorific_value ) if (consumption_and_cost is not None): diff --git a/custom_components/octopus_energy/gas/previous_accumulative_consumption_kwh.py b/custom_components/octopus_energy/gas/previous_accumulative_consumption_kwh.py index 978ac0ed..c546f2ce 100644 --- a/custom_components/octopus_energy/gas/previous_accumulative_consumption_kwh.py +++ b/custom_components/octopus_energy/gas/previous_accumulative_consumption_kwh.py @@ -112,9 +112,7 @@ async def async_update(self): self._last_reset, self._tariff_code, self._native_consumption_units, - self._calorific_value, - # During BST, two records are returned before the rest of the data is available - 3 + self._calorific_value ) if (consumption_and_cost is not None): diff --git a/custom_components/octopus_energy/gas/previous_accumulative_cost.py b/custom_components/octopus_energy/gas/previous_accumulative_cost.py index 95d0c071..022a6c03 100644 --- a/custom_components/octopus_energy/gas/previous_accumulative_cost.py +++ b/custom_components/octopus_energy/gas/previous_accumulative_cost.py @@ -111,9 +111,7 @@ async def async_update(self): self._last_reset, self._tariff_code, self._native_consumption_units, - self._calorific_value, - # During BST, two records are returned before the rest of the data is available - 3 + self._calorific_value ) if (consumption_and_cost is not None): diff --git a/custom_components/octopus_energy/gas/previous_accumulative_cost_override.py b/custom_components/octopus_energy/gas/previous_accumulative_cost_override.py index 9e29396c..15eda01e 100644 --- a/custom_components/octopus_energy/gas/previous_accumulative_cost_override.py +++ b/custom_components/octopus_energy/gas/previous_accumulative_cost_override.py @@ -21,7 +21,7 @@ from .base import (OctopusEnergyGasSensor) -from ..const import DOMAIN +from ..const import DOMAIN, EVENT_GAS_PREVIOUS_CONSUMPTION_OVERRIDE_RATES, MINIMUM_CONSUMPTION_DATA_LENGTH _LOGGER = logging.getLogger(__name__) @@ -112,7 +112,7 @@ async def async_update(self): is_tariff_present = tariff_override_key in self._hass.data[DOMAIN] has_tariff_changed = is_tariff_present and self._hass.data[DOMAIN][tariff_override_key] != self._tariff_code - if (consumption_data is not None and len(consumption_data) > 0 and is_tariff_present and (is_old_data or has_tariff_changed)): + if (consumption_data is not None and len(consumption_data) >= MINIMUM_CONSUMPTION_DATA_LENGTH and is_tariff_present and (is_old_data or has_tariff_changed)): _LOGGER.debug(f"Calculating previous gas consumption cost override for '{self._mprn}/{self._serial_number}'...") tariff_override = self._hass.data[DOMAIN][tariff_override_key] @@ -128,9 +128,7 @@ async def async_update(self): None if has_tariff_changed else self._last_reset, tariff_override, self._native_consumption_units, - self._calorific_value, - # During BST, two records are returned before the rest of the data is available - 3 + self._calorific_value ) self._tariff_code = tariff_override @@ -158,6 +156,8 @@ async def async_update(self): }, consumption_and_cost["charges"])), "calorific_value": self._calorific_value } + + self._hass.bus.async_fire(EVENT_GAS_PREVIOUS_CONSUMPTION_OVERRIDE_RATES, { "mprn": self._mprn, "tariff_code": self._tariff_code, "rates": rate_data }) async def async_added_to_hass(self): """Call when entity about to be added to hass.""" diff --git a/custom_components/octopus_energy/gas/rates_current_day.py b/custom_components/octopus_energy/gas/rates_current_day.py new file mode 100644 index 00000000..8aa57104 --- /dev/null +++ b/custom_components/octopus_energy/gas/rates_current_day.py @@ -0,0 +1,69 @@ +import logging + +from homeassistant.core import HomeAssistant, callback + +from homeassistant.components.event import ( + EventEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from .base import (OctopusEnergyGasSensor) +from ..const import EVENT_GAS_CURRENT_DAY_RATES + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyGasCurrentDayRates(OctopusEnergyGasSensor, EventEntity, RestoreEntity): + """Sensor for displaying the current day's rates.""" + + def __init__(self, hass: HomeAssistant, meter, point): + """Init sensor.""" + # Pass coordinator to base class + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._hass = hass + self._state = None + self._last_updated = None + + self._attr_event_types = [EVENT_GAS_CURRENT_DAY_RATES] + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_current_day_rates" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Current Day Rates" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyGasCurrentDayRates state: {self._state}') + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self._hass.bus.async_listen(self._attr_event_types[0], self._async_handle_event) + + @callback + def _async_handle_event(self, event) -> None: + if (event.data is not None and "mprn" in event.data and event.data["mprn"] == self._mprn): + self._trigger_event(event.event_type, event.data) + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/rates_next_day.py b/custom_components/octopus_energy/gas/rates_next_day.py new file mode 100644 index 00000000..c49cffef --- /dev/null +++ b/custom_components/octopus_energy/gas/rates_next_day.py @@ -0,0 +1,69 @@ +import logging + +from homeassistant.core import HomeAssistant, callback + +from homeassistant.components.event import ( + EventEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from .base import (OctopusEnergyGasSensor) +from ..const import EVENT_GAS_NEXT_DAY_RATES + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyGasNextDayRates(OctopusEnergyGasSensor, EventEntity, RestoreEntity): + """Sensor for displaying the next day's rates.""" + + def __init__(self, hass: HomeAssistant, meter, point): + """Init sensor.""" + # Pass coordinator to base class + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._hass = hass + self._state = None + self._last_updated = None + + self._attr_event_types = [EVENT_GAS_NEXT_DAY_RATES] + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_next_day_rates" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Next Day Rates" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyGasNextDayRates state: {self._state}') + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self._hass.bus.async_listen(self._attr_event_types[0], self._async_handle_event) + + @callback + def _async_handle_event(self, event) -> None: + if (event.data is not None and "mprn" in event.data and event.data["mprn"] == self._mprn): + self._trigger_event(event.event_type, event.data) + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/rates_previous_consumption.py b/custom_components/octopus_energy/gas/rates_previous_consumption.py new file mode 100644 index 00000000..0a6739ba --- /dev/null +++ b/custom_components/octopus_energy/gas/rates_previous_consumption.py @@ -0,0 +1,69 @@ +import logging + +from homeassistant.core import HomeAssistant, callback + +from homeassistant.components.event import ( + EventEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from .base import (OctopusEnergyGasSensor) +from ..const import EVENT_GAS_PREVIOUS_CONSUMPTION_RATES + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyGasPreviousConsumptionRates(OctopusEnergyGasSensor, EventEntity, RestoreEntity): + """Sensor for displaying the previous consumption's rates.""" + + def __init__(self, hass: HomeAssistant, meter, point): + """Init sensor.""" + # Pass coordinator to base class + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._hass = hass + self._state = None + self._last_updated = None + + self._attr_event_types = [EVENT_GAS_PREVIOUS_CONSUMPTION_RATES] + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_previous_consumption_rates" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Previous Consumption Rates" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyGasPreviousConsumptionRates state: {self._state}') + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self._hass.bus.async_listen(self._attr_event_types[0], self._async_handle_event) + + @callback + def _async_handle_event(self, event) -> None: + if (event.data is not None and "mprn" in event.data and event.data["mprn"] == self._mprn): + self._trigger_event(event.event_type, event.data) + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/rates_previous_consumption_override.py b/custom_components/octopus_energy/gas/rates_previous_consumption_override.py new file mode 100644 index 00000000..b870486a --- /dev/null +++ b/custom_components/octopus_energy/gas/rates_previous_consumption_override.py @@ -0,0 +1,69 @@ +import logging + +from homeassistant.core import HomeAssistant, callback + +from homeassistant.components.event import ( + EventEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from .base import (OctopusEnergyGasSensor) +from ..const import EVENT_GAS_PREVIOUS_CONSUMPTION_OVERRIDE_RATES + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyGasPreviousConsumptionOverrideRates(OctopusEnergyGasSensor, EventEntity, RestoreEntity): + """Sensor for displaying the previous consumption override's rates.""" + + def __init__(self, hass: HomeAssistant, meter, point): + """Init sensor.""" + # Pass coordinator to base class + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._hass = hass + self._state = None + self._last_updated = None + + self._attr_event_types = [EVENT_GAS_PREVIOUS_CONSUMPTION_OVERRIDE_RATES] + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_previous_consumption_override_rates" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Previous Consumption Override Rates" + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyGasPreviousConsumptionOverrideRates state: {self._state}') + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self._hass.bus.async_listen(self._attr_event_types[0], self._async_handle_event) + + @callback + def _async_handle_event(self, event) -> None: + if (event.data is not None and "mprn" in event.data and event.data["mprn"] == self._mprn): + self._trigger_event(event.event_type, event.data) + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/gas/rates_previous_day.py b/custom_components/octopus_energy/gas/rates_previous_day.py new file mode 100644 index 00000000..44354151 --- /dev/null +++ b/custom_components/octopus_energy/gas/rates_previous_day.py @@ -0,0 +1,69 @@ +import logging + +from homeassistant.core import HomeAssistant, callback + +from homeassistant.components.event import ( + EventEntity, +) +from homeassistant.helpers.restore_state import RestoreEntity + +from .base import (OctopusEnergyGasSensor) +from ..const import EVENT_GAS_PREVIOUS_DAY_RATES + +_LOGGER = logging.getLogger(__name__) + +class OctopusEnergyGasPreviousDayRates(OctopusEnergyGasSensor, EventEntity, RestoreEntity): + """Sensor for displaying the previous day's rates.""" + + def __init__(self, hass: HomeAssistant, meter, point): + """Init sensor.""" + # Pass coordinator to base class + OctopusEnergyGasSensor.__init__(self, hass, meter, point) + + self._hass = hass + self._state = None + self._last_updated = None + + self._attr_event_types = [EVENT_GAS_PREVIOUS_DAY_RATES] + + @property + def unique_id(self): + """The id of the sensor.""" + return f"octopus_energy_gas_{self._serial_number}_{self._mprn}_previous_day_rates" + + @property + def name(self): + """Name of the sensor.""" + return f"Gas {self._serial_number} {self._mprn} Previous Day Rates" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ + return False + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + state = await self.async_get_last_state() + + if state is not None and self._state is None: + self._state = state.state + self._attributes = {} + for x in state.attributes.keys(): + self._attributes[x] = state.attributes[x] + + _LOGGER.debug(f'Restored OctopusEnergyGasPreviousDayRates state: {self._state}') + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self._hass.bus.async_listen(self._attr_event_types[0], self._async_handle_event) + + @callback + def _async_handle_event(self, event) -> None: + if (event.data is not None and "mprn" in event.data and event.data["mprn"] == self._mprn): + self._trigger_event(event.event_type, event.data) + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/octopus_energy/statistics/refresh.py b/custom_components/octopus_energy/statistics/refresh.py index 9283cad8..1029661b 100644 --- a/custom_components/octopus_energy/statistics/refresh.py +++ b/custom_components/octopus_energy/statistics/refresh.py @@ -52,9 +52,7 @@ async def async_refresh_previous_electricity_consumption_data( rates, 0, None, - tariff_code, - # During BST, two records are returned before the rest of the data is available - 3 + tariff_code ) if consumption_and_cost is not None: @@ -122,9 +120,7 @@ async def async_refresh_previous_gas_consumption_data( None, tariff_code, consumption_units, - calorific_value, - # During BST, two records are returned before the rest of the data is available - 3 + calorific_value ) if consumption_and_cost is not None: diff --git a/tests/integration/coordinators/test_fetch_consumption_and_rates.py b/tests/integration/coordinators/test_fetch_consumption_and_rates.py index 6207acc3..a85b6b18 100644 --- a/tests/integration/coordinators/test_fetch_consumption_and_rates.py +++ b/tests/integration/coordinators/test_fetch_consumption_and_rates.py @@ -45,6 +45,12 @@ async def test_when_now_is_at_30_minute_mark_and_electricity_sensor_then_request minutesStr = f'{minutes}'.zfill(2) current_utc_timestamp = datetime.strptime(f'2022-02-12T00:{minutesStr}:00Z', "%Y-%m-%dT%H:%M:%S%z") + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + # Act result = await async_fetch_consumption_and_rates( previous_data, @@ -56,7 +62,8 @@ async def test_when_now_is_at_30_minute_mark_and_electricity_sensor_then_request sensor_serial_number, is_electricity, tariff_code, - is_smart_meter + is_smart_meter, + fire_event ) # Assert diff --git a/tests/integration/electricity/test_calculate_electricity_consumption_and_cost.py b/tests/integration/electricity/test_calculate_electricity_consumption_and_cost.py index e00b4e63..cfc7b332 100644 --- a/tests/integration/electricity/test_calculate_electricity_consumption_and_cost.py +++ b/tests/integration/electricity/test_calculate_electricity_consumption_and_cost.py @@ -18,6 +18,12 @@ async def test_when_calculate_electricity_cost_uses_real_data_then_calculation_r tariff_code = "E-1R-SUPER-GREEN-24M-21-07-30-A" latest_date = None is_smart_meter = True + + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None # Retrieve real consumption data so we can make sure our calculation works with the result current_utc_timestamp = datetime.strptime(f'2022-03-02T00:00:00Z', "%Y-%m-%dT%H:%M:%S%z") @@ -34,7 +40,8 @@ async def test_when_calculate_electricity_cost_uses_real_data_then_calculation_r sensor_serial_number, is_electricity, tariff_code, - is_smart_meter + is_smart_meter, + fire_event ) assert consumption_and_rates_result is not None diff --git a/tests/integration/gas/test_calculate_gas_consumption_and_cost.py b/tests/integration/gas/test_calculate_gas_consumption_and_cost.py index e9aa71f3..9289ac35 100644 --- a/tests/integration/gas/test_calculate_gas_consumption_and_cost.py +++ b/tests/integration/gas/test_calculate_gas_consumption_and_cost.py @@ -20,6 +20,12 @@ async def test_when_calculate_gas_cost_using_real_data_then_calculation_returned period_to = datetime.strptime("2022-03-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") tariff_code = "G-1R-SUPER-GREEN-24M-21-07-30-A" latest_date = None + + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None # Retrieve real consumption data so we can make sure our calculation works with the result current_utc_timestamp = datetime.strptime(f'2022-03-02T00:00:00Z', "%Y-%m-%dT%H:%M:%S%z") @@ -36,7 +42,8 @@ async def test_when_calculate_gas_cost_using_real_data_then_calculation_returned sensor_serial_number, is_electricity, tariff_code, - True + True, + fire_event ) assert consumption_and_rates_result is not None diff --git a/tests/unit/config/test_validate_target_rate_config.py b/tests/unit/config/test_validate_target_rate_config.py index 8efb568f..aa6b6aa1 100644 --- a/tests/unit/config/test_validate_target_rate_config.py +++ b/tests/unit/config/test_validate_target_rate_config.py @@ -324,6 +324,8 @@ async def test_when_config_is_valid_and_not_agile_then_no_errors_returned(start_ @pytest.mark.asyncio @pytest.mark.parametrize("start_time,end_time,offset",[ ("00:00","23:00","00:00:00"), + ("00:00","16:00","00:00:00"), + ("23:00","16:00","00:00:00"), ("16:00","16:00","00:00:00"), ("16:00","00:00","00:00:00"), (None, "23:00", None), diff --git a/tests/unit/coordinators/test_async_refresh_electricity_rates_data.py b/tests/unit/coordinators/test_async_refresh_electricity_rates_data.py index 1fd557d7..46a9919b 100644 --- a/tests/unit/coordinators/test_async_refresh_electricity_rates_data.py +++ b/tests/unit/coordinators/test_async_refresh_electricity_rates_data.py @@ -6,6 +6,7 @@ from custom_components.octopus_energy.api_client import OctopusEnergyApiClient from custom_components.octopus_energy.coordinators.electricity_rates import async_refresh_electricity_rates_data +from custom_components.octopus_energy.const import EVENT_ELECTRICITY_CURRENT_DAY_RATES, EVENT_ELECTRICITY_NEXT_DAY_RATES, EVENT_ELECTRICITY_PREVIOUS_DAY_RATES current = datetime.strptime("2023-07-14T10:30:01+01:00", "%Y-%m-%dT%H:%M:%S%z") period_from = datetime.strptime("2023-07-14T00:00:00+01:00", "%Y-%m-%dT%H:%M:%S%z") @@ -43,6 +44,17 @@ def get_account_info(is_active_agreement = True): ] } +def assert_raised_events(raised_events: dict, expected_event_name: str, expected_valid_from: datetime, expected_valid_to: datetime): + assert expected_event_name in raised_events + assert "mpan" in raised_events[expected_event_name] + assert raised_events[expected_event_name]["mpan"] == mpan + assert "rates" in raised_events[expected_event_name] + assert len(raised_events[expected_event_name]["rates"]) > 2 + assert "valid_from" in raised_events[expected_event_name]["rates"][0] + assert raised_events[expected_event_name]["rates"][0]["valid_from"] == expected_valid_from + assert "valid_to" in raised_events[expected_event_name]["rates"][-1] + assert raised_events[expected_event_name]["rates"][-1]["valid_to"] == expected_valid_to + @pytest.mark.asyncio async def test_when_account_info_is_none_then_existing_rates_returned(): expected_rates = create_rate_data(period_from, period_to, [1, 2]) @@ -52,6 +64,12 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): rates_returned = True return expected_rates + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = None existing_rates = { mpan: create_rate_data(period_from, period_to, [2, 4]) @@ -65,11 +83,13 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): client, account_info, existing_rates, - dispatches + dispatches, + fire_event ) assert retrieved_rates == existing_rates assert rates_returned == False + assert len(actual_fired_events.keys()) == 0 @pytest.mark.asyncio async def test_when_no_active_rates_then_empty_rates_returned(): @@ -80,6 +100,12 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): rates_returned = True return expected_rates + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = get_account_info(False) existing_rates = { mpan: create_rate_data(period_from, period_to, [2, 4]) @@ -93,11 +119,13 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): client, account_info, existing_rates, - dispatches + dispatches, + fire_event ) assert retrieved_rates == {} assert rates_returned == False + assert len(actual_fired_events.keys()) == 0 @pytest.mark.asyncio async def test_when_current_is_not_thirty_minutes_then_existing_rates_returned(): @@ -113,6 +141,12 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): rates_returned = True return expected_rates + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = get_account_info() existing_rates = { mpan: create_rate_data(period_from, period_to, [2, 4]) @@ -126,25 +160,35 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): client, account_info, existing_rates, - dispatches + dispatches, + fire_event ) assert retrieved_rates == existing_rates assert rates_returned == False + assert len(actual_fired_events.keys()) == 0 @pytest.mark.asyncio async def test_when_existing_rates_is_none_then_rates_retrieved(): - expected_rates = create_rate_data(period_from, period_to, [1, 2]) + expected_period_from = (current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_period_to = (current + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_rates = create_rate_data(expected_period_from, expected_period_to, [1, 2]) rates_returned = False requested_period_from = None requested_period_to = None async def async_mocked_get_electricity_rates(*args, **kwargs): - nonlocal requested_period_from, requested_period_to, rates_returned + nonlocal requested_period_from, requested_period_to, rates_returned, expected_rates requested_client, requested_tariff_code, is_smart_meter, requested_period_from, requested_period_to = args rates_returned = True return expected_rates + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = get_account_info() existing_rates = None expected_retrieved_rates = { @@ -159,23 +203,37 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): client, account_info, existing_rates, - dispatches + dispatches, + fire_event ) assert retrieved_rates == expected_retrieved_rates assert rates_returned == True - assert requested_period_from == (current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) - assert requested_period_to == (current + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) + assert requested_period_from == expected_period_from + assert requested_period_to == expected_period_to + + assert len(actual_fired_events.keys()) == 3 + assert_raised_events(actual_fired_events, EVENT_ELECTRICITY_PREVIOUS_DAY_RATES, requested_period_from, requested_period_from + timedelta(days=1)) + assert_raised_events(actual_fired_events, EVENT_ELECTRICITY_CURRENT_DAY_RATES, requested_period_from + timedelta(days=1), requested_period_from + timedelta(days=2)) + assert_raised_events(actual_fired_events, EVENT_ELECTRICITY_NEXT_DAY_RATES, requested_period_from + timedelta(days=2), requested_period_from + timedelta(days=3)) @pytest.mark.asyncio async def test_when_key_not_in_existing_rates_is_none_then_rates_retrieved(): - expected_rates = create_rate_data(period_from, period_to, [1, 2]) + expected_period_from = (current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_period_to = (current + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_rates = create_rate_data(expected_period_from, expected_period_to, [1, 2]) rates_returned = False async def async_mocked_get_electricity_rates(*args, **kwargs): nonlocal rates_returned rates_returned = True return expected_rates + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = get_account_info() existing_rates = {} expected_retrieved_rates = { @@ -190,21 +248,35 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): client, account_info, existing_rates, - dispatches + dispatches, + fire_event ) assert retrieved_rates == expected_retrieved_rates assert rates_returned == True + + assert len(actual_fired_events.keys()) == 3 + assert_raised_events(actual_fired_events, EVENT_ELECTRICITY_PREVIOUS_DAY_RATES, expected_period_from, expected_period_from + timedelta(days=1)) + assert_raised_events(actual_fired_events, EVENT_ELECTRICITY_CURRENT_DAY_RATES, expected_period_from + timedelta(days=1), expected_period_from + timedelta(days=2)) + assert_raised_events(actual_fired_events, EVENT_ELECTRICITY_NEXT_DAY_RATES, expected_period_from + timedelta(days=2), expected_period_from + timedelta(days=3)) @pytest.mark.asyncio async def test_when_existing_rates_is_old_then_rates_retrieved(): - expected_rates = create_rate_data(period_from, period_to, [1, 2]) + expected_period_from = (current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_period_to = (current + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_rates = create_rate_data(expected_period_from, expected_period_to, [1, 2]) rates_returned = False async def async_mocked_get_electricity_rates(*args, **kwargs): nonlocal rates_returned rates_returned = True return expected_rates + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = get_account_info() existing_rates = { mpan: create_rate_data(period_from - timedelta(days=60), period_to - timedelta(days=60), [2, 4]) @@ -221,29 +293,46 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): client, account_info, existing_rates, - dispatches + dispatches, + fire_event ) assert retrieved_rates == expected_retrieved_rates assert rates_returned == True + + assert len(actual_fired_events.keys()) == 3 + assert_raised_events(actual_fired_events, EVENT_ELECTRICITY_PREVIOUS_DAY_RATES, expected_period_from, expected_period_from + timedelta(days=1)) + assert_raised_events(actual_fired_events, EVENT_ELECTRICITY_CURRENT_DAY_RATES, expected_period_from + timedelta(days=1), expected_period_from + timedelta(days=2)) + assert_raised_events(actual_fired_events, EVENT_ELECTRICITY_NEXT_DAY_RATES, expected_period_from + timedelta(days=2), expected_period_from + timedelta(days=3)) @pytest.mark.asyncio async def test_when_dispatched_rates_provided_then_rates_are_adjusted(): - expected_rates = create_rate_data(period_from, period_to, [1, 2, 3, 4]) + expected_period_from = (current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_period_to = (current + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_rates = create_rate_data(expected_period_from, expected_period_to, [1, 2, 3, 4]) rates_returned = False async def async_mocked_get_electricity_rates(*args, **kwargs): nonlocal rates_returned rates_returned = True return expected_rates + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = get_account_info() existing_rates = {} expected_retrieved_rates = { mpan: expected_rates } + + expected_dispatch_start = datetime.strptime("2023-07-14T02:30:00+01:00", "%Y-%m-%dT%H:%M:%S%z") + expected_dispatch_end = datetime.strptime("2023-07-14T02:30:00+01:00", "%Y-%m-%dT%H:%M:%S%z") dispatches = { "planned": [{ - "start": datetime.strptime("2023-07-14T02:30:00+01:00", "%Y-%m-%dT%H:%M:%S%z"), - "end": datetime.strptime("2023-07-14T03:30:00+01:00", "%Y-%m-%dT%H:%M:%S%z"), + "start": expected_dispatch_start, + "end": expected_dispatch_end, "source": "smart-charge" }], "completed": [] } @@ -254,7 +343,8 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): client, account_info, existing_rates, - dispatches + dispatches, + fire_event ) assert len(retrieved_rates) == len(expected_retrieved_rates) @@ -265,7 +355,7 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): expected_rate = expected_retrieved_rates[mpan][index] actual_rate = retrieved_rates[mpan][index] - if (index == 5 or index == 6): + if actual_rate["valid_from"] >= expected_dispatch_start and actual_rate["valid_to"] <= expected_dispatch_end: assert "is_intelligent_adjusted" in actual_rate assert actual_rate["is_intelligent_adjusted"] == True assert actual_rate["value_inc_vat"] == 1 @@ -274,6 +364,11 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): assert expected_rate == actual_rate assert rates_returned == True + + assert len(actual_fired_events.keys()) == 3 + assert_raised_events(actual_fired_events, EVENT_ELECTRICITY_PREVIOUS_DAY_RATES, expected_period_from, expected_period_from + timedelta(days=1)) + assert_raised_events(actual_fired_events, EVENT_ELECTRICITY_CURRENT_DAY_RATES, expected_period_from + timedelta(days=1), expected_period_from + timedelta(days=2)) + assert_raised_events(actual_fired_events, EVENT_ELECTRICITY_NEXT_DAY_RATES, expected_period_from + timedelta(days=2), expected_period_from + timedelta(days=3)) @pytest.mark.asyncio async def test_when_rates_not_retrieved_then_existing_rates_returned(): @@ -284,6 +379,12 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): rates_returned = True return None + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = get_account_info() existing_rates = { mpan: expected_rates @@ -297,10 +398,12 @@ async def async_mocked_get_electricity_rates(*args, **kwargs): client, account_info, existing_rates, - dispatches + dispatches, + fire_event ) assert retrieved_rates is not None assert mpan in retrieved_rates assert retrieved_rates[mpan] == expected_rates - assert rates_returned == True \ No newline at end of file + assert rates_returned == True + assert len(actual_fired_events.keys()) == 0 \ No newline at end of file diff --git a/tests/unit/coordinators/test_async_refresh_gas_rates_data.py b/tests/unit/coordinators/test_async_refresh_gas_rates_data.py index f1c0128c..213feb1d 100644 --- a/tests/unit/coordinators/test_async_refresh_gas_rates_data.py +++ b/tests/unit/coordinators/test_async_refresh_gas_rates_data.py @@ -6,6 +6,7 @@ from custom_components.octopus_energy.api_client import OctopusEnergyApiClient from custom_components.octopus_energy.coordinators.gas_rates import async_refresh_gas_rates_data +from custom_components.octopus_energy.const import EVENT_GAS_CURRENT_DAY_RATES, EVENT_GAS_NEXT_DAY_RATES, EVENT_GAS_PREVIOUS_DAY_RATES current = datetime.strptime("2023-07-14T10:30:01+01:00", "%Y-%m-%dT%H:%M:%S%z") period_from = datetime.strptime("2023-07-14T00:00:00+01:00", "%Y-%m-%dT%H:%M:%S%z") @@ -43,6 +44,17 @@ def get_account_info(is_active_agreement = True): ] } +def assert_raised_events(raised_events: dict, expected_event_name: str, expected_valid_from: datetime, expected_valid_to: datetime): + assert expected_event_name in raised_events + assert "mprn" in raised_events[expected_event_name] + assert raised_events[expected_event_name]["mprn"] == mprn + assert "rates" in raised_events[expected_event_name] + assert len(raised_events[expected_event_name]["rates"]) > 2 + assert "valid_from" in raised_events[expected_event_name]["rates"][0] + assert raised_events[expected_event_name]["rates"][0]["valid_from"] == expected_valid_from + assert "valid_to" in raised_events[expected_event_name]["rates"][-1] + assert raised_events[expected_event_name]["rates"][-1]["valid_to"] == expected_valid_to + @pytest.mark.asyncio async def test_when_account_info_is_none_then_existing_rates_returned(): expected_rates = create_rate_data(period_from, period_to, [1, 2]) @@ -52,6 +64,12 @@ async def async_mocked_get_gas_rates(*args, **kwargs): rates_returned = True return expected_rates + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = None existing_rates = { mprn: create_rate_data(period_from, period_to, [2, 4]) @@ -63,11 +81,13 @@ async def async_mocked_get_gas_rates(*args, **kwargs): current, client, account_info, - existing_rates + existing_rates, + fire_event ) assert retrieved_rates == existing_rates assert rates_returned == False + assert len(actual_fired_events.keys()) == 0 @pytest.mark.asyncio async def test_when_no_active_rates_then_empty_rates_returned(): @@ -78,6 +98,12 @@ async def async_mocked_get_gas_rates(*args, **kwargs): rates_returned = True return expected_rates + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = get_account_info(False) existing_rates = { mprn: create_rate_data(period_from, period_to, [2, 4]) @@ -89,11 +115,13 @@ async def async_mocked_get_gas_rates(*args, **kwargs): current, client, account_info, - existing_rates + existing_rates, + fire_event ) assert retrieved_rates == {} assert rates_returned == False + assert len(actual_fired_events.keys()) == 0 @pytest.mark.asyncio async def test_when_current_is_not_thirty_minutes_then_existing_rates_returned(): @@ -109,6 +137,12 @@ async def async_mocked_get_gas_rates(*args, **kwargs): rates_returned = True return expected_rates + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = get_account_info() existing_rates = { mprn: create_rate_data(period_from, period_to, [2, 4]) @@ -120,15 +154,19 @@ async def async_mocked_get_gas_rates(*args, **kwargs): current, client, account_info, - existing_rates + existing_rates, + fire_event ) assert retrieved_rates == existing_rates assert rates_returned == False + assert len(actual_fired_events.keys()) == 0 @pytest.mark.asyncio async def test_when_existing_rates_is_none_then_rates_retrieved(): - expected_rates = create_rate_data(period_from, period_to, [1, 2]) + expected_period_from = (current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_period_to = (current + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_rates = create_rate_data(expected_period_from, expected_period_to, [1, 2]) rates_returned = False requested_period_from = None requested_period_to = None @@ -139,6 +177,12 @@ async def async_mocked_get_gas_rates(*args, **kwargs): rates_returned = True return expected_rates + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = get_account_info() existing_rates = None expected_retrieved_rates = { @@ -151,23 +195,37 @@ async def async_mocked_get_gas_rates(*args, **kwargs): current, client, account_info, - existing_rates + existing_rates, + fire_event ) assert retrieved_rates == expected_retrieved_rates assert rates_returned == True - assert requested_period_from == (current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) - assert requested_period_to == (current + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) + assert requested_period_from == expected_period_from + assert requested_period_to == expected_period_to + + assert len(actual_fired_events.keys()) == 3 + assert_raised_events(actual_fired_events, EVENT_GAS_PREVIOUS_DAY_RATES, requested_period_from, requested_period_from + timedelta(days=1)) + assert_raised_events(actual_fired_events, EVENT_GAS_CURRENT_DAY_RATES, requested_period_from + timedelta(days=1), requested_period_from + timedelta(days=2)) + assert_raised_events(actual_fired_events, EVENT_GAS_NEXT_DAY_RATES, requested_period_from + timedelta(days=2), requested_period_from + timedelta(days=3)) @pytest.mark.asyncio async def test_when_key_not_in_existing_rates_is_none_then_rates_retrieved(): - expected_rates = create_rate_data(period_from, period_to, [1, 2]) + expected_period_from = (current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_period_to = (current + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_rates = create_rate_data(expected_period_from, expected_period_to, [1, 2]) rates_returned = False async def async_mocked_get_gas_rates(*args, **kwargs): nonlocal rates_returned rates_returned = True return expected_rates + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = get_account_info() existing_rates = {} expected_retrieved_rates = { @@ -180,21 +238,35 @@ async def async_mocked_get_gas_rates(*args, **kwargs): current, client, account_info, - existing_rates + existing_rates, + fire_event ) assert retrieved_rates == expected_retrieved_rates assert rates_returned == True + + assert len(actual_fired_events.keys()) == 3 + assert_raised_events(actual_fired_events, EVENT_GAS_PREVIOUS_DAY_RATES, expected_period_from, expected_period_from + timedelta(days=1)) + assert_raised_events(actual_fired_events, EVENT_GAS_CURRENT_DAY_RATES, expected_period_from + timedelta(days=1), expected_period_from + timedelta(days=2)) + assert_raised_events(actual_fired_events, EVENT_GAS_NEXT_DAY_RATES, expected_period_from + timedelta(days=2), expected_period_from + timedelta(days=3)) @pytest.mark.asyncio async def test_when_existing_rates_is_old_then_rates_retrieved(): - expected_rates = create_rate_data(period_from, period_to, [1, 2]) + expected_period_from = (current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_period_to = (current + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) + expected_rates = create_rate_data(expected_period_from, expected_period_to, [1, 2]) rates_returned = False async def async_mocked_get_gas_rates(*args, **kwargs): nonlocal rates_returned rates_returned = True return expected_rates + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = get_account_info() existing_rates = { mprn: create_rate_data(period_from - timedelta(days=60), period_to - timedelta(days=60), [2, 4]) @@ -209,11 +281,17 @@ async def async_mocked_get_gas_rates(*args, **kwargs): current, client, account_info, - existing_rates + existing_rates, + fire_event ) assert retrieved_rates == expected_retrieved_rates assert rates_returned == True + + assert len(actual_fired_events.keys()) == 3 + assert_raised_events(actual_fired_events, EVENT_GAS_PREVIOUS_DAY_RATES, expected_period_from, expected_period_from + timedelta(days=1)) + assert_raised_events(actual_fired_events, EVENT_GAS_CURRENT_DAY_RATES, expected_period_from + timedelta(days=1), expected_period_from + timedelta(days=2)) + assert_raised_events(actual_fired_events, EVENT_GAS_NEXT_DAY_RATES, expected_period_from + timedelta(days=2), expected_period_from + timedelta(days=3)) @pytest.mark.asyncio async def test_when_rates_not_retrieved_then_existing_rates_returned(): @@ -224,6 +302,12 @@ async def async_mocked_get_gas_rates(*args, **kwargs): rates_returned = True return None + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + account_info = get_account_info() existing_rates = { mprn: expected_rates @@ -235,10 +319,12 @@ async def async_mocked_get_gas_rates(*args, **kwargs): current, client, account_info, - existing_rates + existing_rates, + fire_event ) assert retrieved_rates is not None assert mprn in retrieved_rates assert retrieved_rates[mprn] == expected_rates - assert rates_returned == True \ No newline at end of file + assert rates_returned == True + assert len(actual_fired_events.keys()) == 0 \ No newline at end of file diff --git a/tests/unit/coordinators/test_previous_consumption_and_rates.py b/tests/unit/coordinators/test_previous_consumption_and_rates.py index f6671476..8d795419 100644 --- a/tests/unit/coordinators/test_previous_consumption_and_rates.py +++ b/tests/unit/coordinators/test_previous_consumption_and_rates.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +from custom_components.octopus_energy.const import EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_RATES, EVENT_GAS_PREVIOUS_CONSUMPTION_RATES import pytest import mock @@ -7,6 +8,24 @@ from custom_components.octopus_energy.coordinators.previous_consumption_and_rates import async_fetch_consumption_and_rates from custom_components.octopus_energy.api_client import OctopusEnergyApiClient +def assert_raised_events( + raised_events: dict, + expected_event_name: str, + expected_valid_from: datetime, + expected_valid_to: datetime, + expected_identifier: str, + expected_identifier_value: str +): + assert expected_event_name in raised_events + assert expected_identifier in raised_events[expected_event_name] + assert raised_events[expected_event_name][expected_identifier] == expected_identifier_value + assert "rates" in raised_events[expected_event_name] + assert len(raised_events[expected_event_name]["rates"]) > 2 + assert "valid_from" in raised_events[expected_event_name]["rates"][0] + assert raised_events[expected_event_name]["rates"][0]["valid_from"] == expected_valid_from + assert "valid_to" in raised_events[expected_event_name]["rates"][-1] + assert raised_events[expected_event_name]["rates"][-1]["valid_to"] == expected_valid_to + @pytest.mark.asyncio async def test_when_now_is_not_at_30_minute_mark_and_previous_data_is_available_then_previous_data_returned(): # Arrange @@ -29,6 +48,12 @@ async def test_when_now_is_not_at_30_minute_mark_and_previous_data_is_available_ for minute in range(0, 59): if (minute == 0 or minute == 30): continue + + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None minuteStr = f'{minute}'.zfill(2) current_utc_timestamp = datetime.strptime(f'2022-02-12T00:{minuteStr}:00Z', "%Y-%m-%dT%H:%M:%S%z") @@ -44,12 +69,15 @@ async def test_when_now_is_not_at_30_minute_mark_and_previous_data_is_available_ sensor_serial_number, is_electricity, tariff_code, - is_smart_meter + is_smart_meter, + fire_event ) # Assert assert result == previous_data + assert len(actual_fired_events) == 0 + @pytest.mark.asyncio @pytest.mark.parametrize("minutes",[ (0), @@ -76,6 +104,12 @@ async def test_when_now_is_at_30_minute_mark_and_previous_data_is_in_requested_p minutesStr = f'{minutes}'.zfill(2) current_utc_timestamp = datetime.strptime(f'2022-02-12T00:{minutesStr}:00Z', "%Y-%m-%dT%H:%M:%S%z") + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + # Act result = await async_fetch_consumption_and_rates( previous_data, @@ -87,12 +121,15 @@ async def test_when_now_is_at_30_minute_mark_and_previous_data_is_in_requested_p sensor_serial_number, is_electricity, tariff_code, - is_smart_meter + is_smart_meter, + fire_event ) # Assert assert result == previous_data + assert len(actual_fired_events) == 0 + @pytest.mark.asyncio @pytest.mark.parametrize("minutes,previous_data_available",[ (0, True), @@ -118,6 +155,12 @@ async def async_mocked_get_gas_standing_charge(*args, **kwargs): "value_inc_vat": expected_standing_charge } + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_consumption=async_mocked_get_gas_consumption, async_get_gas_rates=async_mocked_get_gas_rates, async_get_gas_standing_charge=async_mocked_get_gas_standing_charge): client = OctopusEnergyApiClient("NOT_REAL") @@ -157,7 +200,8 @@ async def async_mocked_get_gas_standing_charge(*args, **kwargs): sensor_serial_number, is_electricity, tariff_code, - is_smart_meter + is_smart_meter, + fire_event ) # Assert @@ -184,6 +228,14 @@ async def async_mocked_get_gas_standing_charge(*args, **kwargs): assert "standing_charge" in result assert result["standing_charge"] == expected_standing_charge + assert len(actual_fired_events) == 1 + assert_raised_events(actual_fired_events, + EVENT_GAS_PREVIOUS_CONSUMPTION_RATES, + period_from, + period_to, + "mprn", + sensor_identifier) + @pytest.mark.asyncio @pytest.mark.parametrize("minutes,previous_data_available",[ (0, True), @@ -209,6 +261,12 @@ async def async_mocked_get_electricity_standing_charge(*args, **kwargs): "value_inc_vat": expected_standing_charge } + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_consumption=async_mocked_get_electricity_consumption, async_get_electricity_rates=async_mocked_get_electricity_rates, async_get_electricity_standing_charge=async_mocked_get_electricity_standing_charge): client = OctopusEnergyApiClient("NOT_REAL") @@ -250,7 +308,8 @@ async def async_mocked_get_electricity_standing_charge(*args, **kwargs): sensor_serial_number, is_electricity, tariff_code, - is_smart_meter + is_smart_meter, + fire_event ) # Assert @@ -277,6 +336,14 @@ async def async_mocked_get_electricity_standing_charge(*args, **kwargs): assert "standing_charge" in result assert result["standing_charge"] == expected_standing_charge + assert len(actual_fired_events) == 1 + assert_raised_events(actual_fired_events, + EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_RATES, + period_from, + period_to, + "mpan", + sensor_identifier) + @pytest.mark.asyncio @pytest.mark.parametrize("minutes",[ (0), @@ -289,6 +356,12 @@ async def async_mocked_get_gas_consumption(*args, **kwargs): async def async_mocked_get_gas_standing_charge(*args, **kwargs): return None + + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_consumption=async_mocked_get_gas_consumption, async_get_gas_standing_charge=async_mocked_get_gas_standing_charge): client = OctopusEnergyApiClient("NOT_REAL") @@ -331,12 +404,15 @@ async def async_mocked_get_gas_standing_charge(*args, **kwargs): sensor_serial_number, is_electricity, tariff_code, - is_smart_meter + is_smart_meter, + fire_event ) # Assert assert result == previous_data + assert len(actual_fired_events) == 0 + @pytest.mark.asyncio @pytest.mark.parametrize("minutes",[ (0), @@ -349,6 +425,12 @@ async def async_mocked_get_electricity_consumption(*args, **kwargs): async def async_mocked_get_electricity_standing_charge(*args, **kwargs): return None + + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_consumption=async_mocked_get_electricity_consumption, async_get_electricity_standing_charge=async_mocked_get_electricity_standing_charge): client = OctopusEnergyApiClient("NOT_REAL") @@ -395,8 +477,84 @@ async def async_mocked_get_electricity_standing_charge(*args, **kwargs): sensor_serial_number, is_electricity, tariff_code, - is_smart_meter + is_smart_meter, + fire_event ) # Assert - assert result == previous_data \ No newline at end of file + assert result == previous_data + + assert len(actual_fired_events) == 0 + +@pytest.mark.asyncio +async def test_when_not_enough_consumption_returned_then_previous_data_returned(): + # Arrange + period_from = datetime.strptime("2022-02-28T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + period_to = datetime.strptime("2022-03-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + + async def async_mocked_get_electricity_consumption(*args, **kwargs): + return create_consumption_data(period_from, period_to)[:2] + + expected_rates = create_rate_data(period_from, period_to, [1, 2]) + async def async_mocked_get_electricity_rates(*args, **kwargs): + return expected_rates + + expected_standing_charge = 100.2 + async def async_mocked_get_electricity_standing_charge(*args, **kwargs): + return { + "value_inc_vat": expected_standing_charge + } + + actual_fired_events = {} + def fire_event(name, metadata): + nonlocal actual_fired_events + actual_fired_events[name] = metadata + return None + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_consumption=async_mocked_get_electricity_consumption, async_get_electricity_rates=async_mocked_get_electricity_rates, async_get_electricity_standing_charge=async_mocked_get_electricity_standing_charge): + client = OctopusEnergyApiClient("NOT_REAL") + + sensor_identifier = "ABC123" + sensor_serial_number = "123456" + is_electricity = True + tariff_code = "AB-123" + is_smart_meter = True + + period_from = datetime.strptime("2022-02-28T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + period_to = datetime.strptime("2022-03-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + previous_data = None + # Make our previous data for the previous period + previous_data = { + "consumption": create_consumption_data( + datetime.strptime("2022-02-27T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), + datetime.strptime("2022-02-28T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + ), + "rates": create_rate_data( + datetime.strptime("2022-02-27T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), + datetime.strptime("2022-02-28T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), + [1, 2] + ), + "standing_charge": 10.1 + } + + current_utc_timestamp = datetime.strptime(f'2022-02-12T00:00:00Z', "%Y-%m-%dT%H:%M:%S%z") + + # Act + result = await async_fetch_consumption_and_rates( + previous_data, + current_utc_timestamp, + client, + period_from, + period_to, + sensor_identifier, + sensor_serial_number, + is_electricity, + tariff_code, + is_smart_meter, + fire_event + ) + + # Assert + assert result == previous_data + + assert len(actual_fired_events) == 0 \ No newline at end of file diff --git a/tests/unit/electricity/test_calculate_electricity_consumption_and_cost.py b/tests/unit/electricity/test_calculate_electricity_consumption_and_cost.py index 1060ffae..15a2f962 100644 --- a/tests/unit/electricity/test_calculate_electricity_consumption_and_cost.py +++ b/tests/unit/electricity/test_calculate_electricity_consumption_and_cost.py @@ -53,33 +53,6 @@ async def test_when_electricity_rates_is_none_then_no_calculation_is_returned(): # Assert assert result is None -@pytest.mark.asyncio -async def test_when_electricity_consumption_is_less_than_three_records_then_no_calculation_is_returned(): - # Arrange - period_from = datetime.strptime("2022-02-28T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - period_to = datetime.strptime("2022-03-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - latest_date = datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - tariff_code = "E-1R-SUPER-GREEN-24M-21-07-30-A" - consumption_data = create_consumption_data( - datetime.strptime("2022-02-28T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - datetime.strptime("2022-02-28T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - ) - rate_data = create_rate_data(period_from, period_to, [1, 2]) - standing_charge = 10.1 - - # Act - result = calculate_electricity_consumption_and_cost( - consumption_data, - rate_data, - standing_charge, - latest_date, - tariff_code, - 3 - ) - - # Assert - assert result is None - @pytest.mark.asyncio async def test_when_electricity_consumption_is_before_latest_date_then_no_calculation_is_returned(): # Arrange diff --git a/tests/unit/gas/test_calculate_gas_consumption_and_cost.py b/tests/unit/gas/test_calculate_gas_consumption_and_cost.py index 8dc7018b..76e18f52 100644 --- a/tests/unit/gas/test_calculate_gas_consumption_and_cost.py +++ b/tests/unit/gas/test_calculate_gas_consumption_and_cost.py @@ -59,37 +59,6 @@ async def test_when_gas_rates_is_none_then_no_calculation_is_returned(): # Assert assert consumption_cost is None -@pytest.mark.asyncio -async def test_when_gas_consumption_is_less_than_three_records_then_no_calculation_is_returned(): - # Arrange - latest_date = datetime.strptime("2022-02-09T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - tariff_code = "G-1R-SUPER-GREEN-24M-21-07-30-A" - consumption_data = create_consumption_data( - datetime.strptime("2022-02-28T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - datetime.strptime("2022-02-28T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - ) - rates_data = create_rate_data( - datetime.strptime("2022-02-28T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - datetime.strptime("2022-02-28T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), - [1, 2] - ) - standing_charge = 27.0 - - # Act - consumption_cost = calculate_gas_consumption_and_cost( - consumption_data, - rates_data, - standing_charge, - latest_date, - tariff_code, - "m³", - 40, - 3 - ) - - # Assert - assert consumption_cost is None - @pytest.mark.asyncio async def test_when_gas_consumption_is_before_latest_date_then_no_calculation_is_returned(): # Arrange