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 `- - sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_current_rate + event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_current_day_rates type: column name: '' color: yellow @@ -45,7 +45,23 @@ type: custom:apexcharts-card in_header: false legend_value: false data_generator: | - return entity.attributes.all_rates.map((entry) => { + return entity.attributes.rates.map((entry) => { + return [new Date(entry.valid_from), entry.value_inc_vat]; + }); + offset: '-15min' + - entity: >- + event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_next_day_rates + type: column + name: '' + color: yellow + opacity: 1 + stroke_width: 0 + unit: W + show: + in_header: false + legend_value: false + data_generator: | + return entity.attributes.rates.map((entry) => { return [new Date(entry.valid_from), entry.value_inc_vat]; }); offset: '-15min' @@ -138,9 +154,12 @@ If you're looking to combine import and export rates then create a card with the ```yaml type: custom:config-template-card entities: - - sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_current_rate + - event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_current_day_rates + - event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_next_day_rates + - >- + event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_export_current_day_rates - >- - sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_export_current_rate + event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_export_next_day_rates card: card_mod: style: | @@ -169,19 +188,34 @@ card: stroke_width: 2 fill_raw: 'null' series: - - entity: sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_current_rate - name: Import + - entity: event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_current_day_rates + name: Import today + curve: stepline + data_generator: | + return entity.attributes.rates.map((entry) => { + return [new Date(entry.valid_from), entry.value_inc_vat/100]; + }); + - entity: event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_next_day_rates + name: Import tomorrow + curve: stepline + data_generator: | + return entity.attributes.rates.map((entry) => { + return [new Date(entry.valid_from), entry.value_inc_vat/100]; + }); + - entity: >- + event.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_export_current_day_rates + name: Export curve: stepline data_generator: | - return entity.attributes.all_rates.map((entry) => { + return entity.attributes.rates.map((entry) => { return [new Date(entry.valid_from), entry.value_inc_vat/100]; }); - entity: >- - sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_export_current_rate + sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_export_next_day_rates name: Export curve: stepline data_generator: | - return entity.attributes.all_rates.map((entry) => { + return entity.attributes.rates.map((entry) => { return [new Date(entry.valid_from), entry.value_inc_vat/100]; }); apex_config: diff --git a/_docs/events.md b/_docs/events.md new file mode 100644 index 00000000..7ca333c6 --- /dev/null +++ b/_docs/events.md @@ -0,0 +1,162 @@ +# Events + +- [Events](#events) + - [Electricity Current Day Rates](#electricity-current-day-rates) + - [Electricity Previous Day Rates](#electricity-previous-day-rates) + - [Electricity Next Day Rates](#electricity-next-day-rates) + - [Electricity Previous Consumption Rates](#electricity-previous-consumption-rates) + - [Electricity Previous Consumption Override Rates](#electricity-previous-consumption-override-rates) + - [Gas Current Day Rates](#gas-current-day-rates) + - [Gas Previous Day Rates](#gas-previous-day-rates) + - [Gas Next Day Rates](#gas-next-day-rates) + - [Gas Previous Consumption Rates](#gas-previous-consumption-rates) + - [Gas Previous Consumption Override Rates](#gas-previous-consumption-override-rates) + +The following events are raised by the integration. These events power various sensors mentioned above. They can also be used to trigger automations. An example automation might look like the following + +```yaml +- alias: OE rates change + trigger: + - platform: event + event_type: octopus_energy_electricity_next_day_rates + condition: [] + action: + - event: notify_channels + event_data_template: + mode: message + title: OE price changes + message: > + 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 | +| `serial_number` | `string` | The serial number 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 | +| `serial_number` | `string` | The serial number 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 | +| `serial_number` | `string` | The serial number 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 | +| `serial_number` | `string` | The serial number 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 | +| `serial_number` | `string` | The serial number 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 | +| `serial_number` | `string` | The serial number 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 | +| `serial_number` | `string` | The serial number 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 | +| `serial_number` | `string` | The serial number 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 | +| `serial_number` | `string` | The serial number 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 | +| `serial_number` | `string` | The serial number 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/_docs/setup_account.md b/_docs/setup_account.md index 8510e5f1..bd299d8a 100644 --- a/_docs/setup_account.md +++ b/_docs/setup_account.md @@ -12,6 +12,7 @@ - [Government Pricing Caps](#government-pricing-caps) - [Services](#services) - [Service octopus\_energy.purge\_invalid\_external\_statistic\_ids](#service-octopus_energypurge_invalid_external_statistic_ids) + - [Service octopus\_energy.refresh\_previous\_consumption\_data](#service-octopus_energyrefresh_previous_consumption_data) Setup is done entirely via the [integration UI](https://my.home-assistant.io/redirect/config_flow_start/?domain=octopus_energy). @@ -70,4 +71,13 @@ There has been inconsistencies across tariffs on whether government pricing caps ### Service octopus_energy.purge_invalid_external_statistic_ids -Service for removing all external statistics that are associated with meters that don't have an active tariff. This is useful if you've been using the integration and obtained new smart meters. \ No newline at end of file +Service for removing all external statistics that are associated with meters that don't have an active tariff. This is useful if you've been using the integration and obtained new smart meters. + +### Service octopus_energy.refresh_previous_consumption_data + +Service for refreshing the consumption/cost information for a given previous consumption entity. This is useful when you've just installed the integration and want old data brought in or a previous consumption sensor fails to import (e.g. data becomes available outside of the configured offset). The service will raise a notification when the refreshing starts and finishes. + +This service is only available for the following sensors + +- `sensor.octopus_energy_electricity_{{METER_SERIAL_NUMBER}}_{{MPAN_NUMBER}}_previous_accumulative_consumption` +- `sensor.octopus_energy_gas_{{METER_SERIAL_NUMBER}}_{{MPRN_NUMBER}}_previous_accumulative_consumption` \ No newline at end of file diff --git a/custom_components/octopus_energy/__init__.py b/custom_components/octopus_energy/__init__.py index 37b8e159..a2e0272b 100644 --- a/custom_components/octopus_energy/__init__.py +++ b/custom_components/octopus_energy/__init__.py @@ -1,6 +1,7 @@ import logging import asyncio from datetime import timedelta +from custom_components.octopus_energy.utils import get_active_tariff_code from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -28,11 +29,14 @@ CONFIG_TARGET_NAME, DATA_CLIENT, - DATA_ELECTRICITY_RATES_COORDINATOR, + DATA_ELECTRICITY_RATES_COORDINATOR_KEY, DATA_ACCOUNT_ID, DATA_ACCOUNT ) +ACCOUNT_PLATFORMS = ["sensor", "binary_sensor", "text", "number", "switch", "time", "event"] +TARGET_RATE_PLATFORMS = ["binary_sensor"] + from .api_client import OctopusEnergyApiClient _LOGGER = logging.getLogger(__name__) @@ -76,38 +80,25 @@ async def async_setup_entry(hass, entry): if CONFIG_MAIN_API_KEY in config: await async_setup_dependencies(hass, config) - # Forward our entry to setup our default sensors - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "binary_sensor") - ) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "text") - ) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "number") - ) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "switch") - ) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "time") - ) + await hass.config_entries.async_forward_entry_setups(entry, ACCOUNT_PLATFORMS) 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") - - # Forward our entry to setup our target rate sensors - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "binary_sensor") - ) + if DOMAIN not in hass.data or DATA_ACCOUNT not in hass.data[DOMAIN]: + raise ConfigEntryNotReady("Account has not been setup") + + now = utcnow() + account_info = hass.data[DOMAIN][DATA_ACCOUNT] + for point in account_info["electricity_meter_points"]: + # We only care about points that have active agreements + electricity_tariff_code = get_active_tariff_code(now, point["agreements"]) + if electricity_tariff_code is not None: + for meter in point["meters"]: + mpan = point["mpan"] + serial_number = meter["serial_number"] + electricity_rates_coordinator_key = DATA_ELECTRICITY_RATES_COORDINATOR_KEY.format(mpan, serial_number) + if electricity_rates_coordinator_key not in hass.data[DOMAIN]: + raise ConfigEntryNotReady(f"Electricity rates have not been setup for {mpan}/{serial_number}") + + await hass.config_entries.async_forward_entry_setups(entry, TARGET_RATE_PLATFORMS) entry.async_on_unload(entry.add_update_listener(options_update_listener)) @@ -148,14 +139,25 @@ async def async_setup_dependencies(hass, config): if device is not None: device_registry.async_remove_device(device.id) + now = utcnow() + account_info = hass.data[DOMAIN][DATA_ACCOUNT] + for point in account_info["electricity_meter_points"]: + # We only care about points that have active agreements + electricity_tariff_code = get_active_tariff_code(now, point["agreements"]) + if electricity_tariff_code is not None: + for meter in point["meters"]: + mpan = point["mpan"] + serial_number = meter["serial_number"] + is_export_meter = meter["is_export"] + is_smart_meter = meter["is_smart_meter"] + await async_setup_electricity_rates_coordinator(hass, mpan, serial_number, is_smart_meter, is_export_meter) + await async_setup_account_info_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) await async_setup_intelligent_dispatches_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) await async_setup_intelligent_settings_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) - await async_setup_electricity_rates_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) - await async_setup_saving_sessions_coordinators(hass) async def options_update_listener(hass, entry): @@ -167,24 +169,9 @@ async def async_unload_entry(hass, entry): unload_ok = False if CONFIG_MAIN_API_KEY in entry.data: - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, "sensor"), - hass.config_entries.async_forward_entry_unload(entry, "binary_sensor"), - hass.config_entries.async_forward_entry_unload(entry, "text"), - hass.config_entries.async_forward_entry_unload(entry, "number"), - hass.config_entries.async_forward_entry_unload(entry, "switch"), - hass.config_entries.async_forward_entry_unload(entry, "time") - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, ACCOUNT_PLATFORMS) elif CONFIG_TARGET_NAME in entry.data: - unload_ok = all( - await asyncio.gather( - *[hass.config_entries.async_forward_entry_unload(entry, "binary_sensor")] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, TARGET_RATE_PLATFORMS) return unload_ok diff --git a/custom_components/octopus_energy/api_client.py b/custom_components/octopus_energy/api_client.py index 2e68616b..86a74cfb 100644 --- a/custom_components/octopus_energy/api_client.py +++ b/custom_components/octopus_energy/api_client.py @@ -535,7 +535,7 @@ async def async_get_electricity_day_night_rates(self, product_code, tariff_code, return results - async def async_get_electricity_rates(self, tariff_code, is_smart_meter, period_from, period_to): + async def async_get_electricity_rates(self, tariff_code: str, is_smart_meter: bool, period_from: datetime, period_to: datetime): """Get the current rates""" tariff_parts = get_tariff_parts(tariff_code) @@ -632,9 +632,6 @@ async def async_get_electricity_standing_charge(self, tariff_code, period_from, return None product_code = tariff_parts.product_code - - if self.__is_tracker_tariff__(tariff_code): - return await self.__async_get_tracker_standing_charge__(tariff_code, period_from, period_to) result = None async with aiohttp.ClientSession() as client: @@ -659,9 +656,6 @@ async def async_get_gas_standing_charge(self, tariff_code, period_from, period_t product_code = tariff_parts.product_code - if self.__is_tracker_tariff__(tariff_code): - return await self.__async_get_tracker_standing_charge__(tariff_code, period_from, period_to) - result = None async with aiohttp.ClientSession() as client: auth = aiohttp.BasicAuth(self._api_key, '') @@ -888,18 +882,6 @@ async def async_get_intelligent_device(self, account_id: str): return None - def __is_tracker_tariff__(self, tariff_code): - tariff_parts = get_tariff_parts(tariff_code) - if tariff_parts is None: - return None - - product_code = tariff_parts.product_code - - if product_code in self._product_tracker_cache: - return self._product_tracker_cache[product_code] - - return False - def __get_interval_end(self, item): return item["interval_end"] diff --git a/custom_components/octopus_energy/binary_sensor.py b/custom_components/octopus_energy/binary_sensor.py index 4c087897..00286869 100644 --- a/custom_components/octopus_energy/binary_sensor.py +++ b/custom_components/octopus_energy/binary_sensor.py @@ -1,4 +1,3 @@ -from datetime import timedelta import logging import voluptuous as vol @@ -24,7 +23,7 @@ CONFIG_TARGET_NAME, CONFIG_TARGET_MPAN, - DATA_ELECTRICITY_RATES_COORDINATOR, + DATA_ELECTRICITY_RATES_COORDINATOR_KEY, DATA_SAVING_SESSIONS_COORDINATOR, DATA_ACCOUNT ) @@ -74,28 +73,29 @@ async def async_setup_main_sensors(hass, entry, async_add_entities): account_info = hass.data[DOMAIN][DATA_ACCOUNT] - electricity_rate_coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] - now = utcnow() has_intelligent_tariff = False intelligent_mpan = None + intelligent_serial_number = None entities = [OctopusEnergySavingSessions(hass, saving_session_coordinator)] if len(account_info["electricity_meter_points"]) > 0: - electricity_rate_coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] - await electricity_rate_coordinator.async_config_entry_first_refresh() 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"]: + mpan = point["mpan"] + serial_number = meter["serial_number"] + electricity_rate_coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR_KEY.format(mpan, serial_number)] + entities.append(OctopusEnergyElectricityOffPeak(hass, electricity_rate_coordinator, meter, point)) if meter["is_export"] == False: - intelligent_mpan = point["mpan"] - - if is_intelligent_tariff(tariff_code): - has_intelligent_tariff = True - break + + if is_intelligent_tariff(tariff_code): + intelligent_mpan = mpan + intelligent_serial_number = serial_number + has_intelligent_tariff = True should_mock_intelligent_data = await async_mock_intelligent_data(hass) if has_intelligent_tariff or should_mock_intelligent_data: @@ -108,6 +108,7 @@ async def async_setup_main_sensors(hass, entry, async_add_entities): else: device = await client.async_get_intelligent_device(account_id) + electricity_rate_coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR_KEY.format(intelligent_mpan, intelligent_serial_number)] entities.append(OctopusEnergyIntelligentDispatching(hass, coordinator, electricity_rate_coordinator, intelligent_mpan, device)) if len(entities) > 0: @@ -118,18 +119,22 @@ async def async_setup_target_sensors(hass, entry, async_add_entities): if entry.options: config.update(entry.options) - - coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] account_info = hass.data[DOMAIN][DATA_ACCOUNT] mpan = config[CONFIG_TARGET_MPAN] + now = utcnow() is_export = False for point in account_info["electricity_meter_points"]: - if point["mpan"] == mpan: - for meter in point["meters"]: - is_export = meter["is_export"] - - entities = [OctopusEnergyTargetRate(hass, coordinator, config, is_export)] - async_add_entities(entities, True) + tariff_code = get_active_tariff_code(now, point["agreements"]) + if tariff_code is not None: + # For backwards compatibility, pick the first applicable meter + if point["mpan"] == mpan or mpan is None: + for meter in point["meters"]: + is_export = meter["is_export"] + serial_number = meter["serial_number"] + coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR_KEY.format(mpan, serial_number)] + entities = [OctopusEnergyTargetRate(hass, coordinator, config, is_export)] + async_add_entities(entities, True) + return diff --git a/custom_components/octopus_energy/const.py b/custom_components/octopus_energy/const.py index 03de34b1..74737ca5 100644 --- a/custom_components/octopus_energy/const.py +++ b/custom_components/octopus_energy/const.py @@ -33,8 +33,8 @@ CONFIG_TARGET_INVERT_TARGET_RATES = "target_invert_target_rates" DATA_CONFIG = "CONFIG" -DATA_ELECTRICITY_RATES_COORDINATOR = "ELECTRICITY_RATES_COORDINATOR" -DATA_ELECTRICITY_RATES = "ELECTRICITY_RATES" +DATA_ELECTRICITY_RATES_COORDINATOR_KEY = "ELECTRICITY_RATES_COORDINATOR_{}_{}" +DATA_ELECTRICITY_RATES_KEY = "ELECTRICITY_RATES_{}_{}" DATA_CLIENT = "CLIENT" DATA_GAS_TARIFF_CODE = "GAS_TARIFF_CODE" DATA_ACCOUNT_ID = "ACCOUNT_ID" @@ -43,18 +43,16 @@ DATA_SAVING_SESSIONS = "SAVING_SESSIONS" DATA_SAVING_SESSIONS_COORDINATOR = "SAVING_SESSIONS_COORDINATOR" DATA_KNOWN_TARIFF = "KNOWN_TARIFF" -DATA_GAS_RATES_COORDINATOR = "DATA_GAS_RATES_COORDINATOR" -DATA_GAS_RATES = "GAS_RATES" +DATA_GAS_RATES_COORDINATOR_KEY = "DATA_GAS_RATES_COORDINATOR_{}_{}" +DATA_GAS_RATES_KEY = "GAS_RATES_{}_{}" DATA_INTELLIGENT_DISPATCHES = "INTELLIGENT_DISPATCHES" DATA_INTELLIGENT_DISPATCHES_COORDINATOR = "INTELLIGENT_DISPATCHES_COORDINATOR" DATA_INTELLIGENT_SETTINGS = "INTELLIGENT_SETTINGS" DATA_INTELLIGENT_SETTINGS_COORDINATOR = "INTELLIGENT_SETTINGS_COORDINATOR" -DATA_ELECTRICITY_STANDING_CHARGES_COORDINATOR = "ELECTRICITY_STANDING_CHARGES_COORDINATOR" -DATA_ELECTRICITY_STANDING_CHARGES = "ELECTRICITY_STANDING_CHARGES" +DATA_ELECTRICITY_STANDING_CHARGE_KEY = "ELECTRICITY_STANDING_CHARGES_{}_{}" -DATA_GAS_STANDING_CHARGES_COORDINATOR = "GAS_STANDING_CHARGES_COORDINATOR" -DATA_GAS_STANDING_CHARGES = "GAS_STANDING_CHARGES" +DATA_GAS_STANDING_CHARGE_KEY = "GAS_STANDING_CHARGES_{}_{}" STORAGE_COMPLETED_DISPATCHES_NAME = "octopus_energy.{}-completed-intelligent-dispatches.json" @@ -67,6 +65,7 @@ # However it looks like there are some tariffs that don't fit this mold REGEX_TARIFF_PARTS = "^((?P[A-Z])-(?P[0-9A-Z]+)-)?(?P[A-Z0-9-]+)-(?P[A-Z])$" REGEX_OFFSET_PARTS = "^(-)?([0-1]?[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$" +REGEX_DATE = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" DATA_SCHEMA_ACCOUNT = vol.Schema({ vol.Required(CONFIG_MAIN_API_KEY): str, @@ -80,3 +79,20 @@ 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 + +COORDINATOR_REFRESH_IN_SECONDS = 60 \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/__init__.py b/custom_components/octopus_energy/coordinators/__init__.py index edd2347b..c1377a9f 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 @@ -59,34 +58,59 @@ async def async_check_valid_tariff(hass, client: OctopusEnergyApiClient, tariff_ except: _LOGGER.debug(f"Failed to retrieve product info for '{tariff_parts.product_code}'") -def get_current_electricity_agreement_tariff_codes(current: datetime, account_info): - tariff_codes = {} - if account_info is not None and len(account_info["electricity_meter_points"]) > 0: +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) + +def get_electricity_meter_tariff_code(current: datetime, account_info, target_mpan: str, target_serial_number: str): + if len(account_info["electricity_meter_points"]) > 0: for point in account_info["electricity_meter_points"]: active_tariff_code = get_active_tariff_code(current, point["agreements"]) # The type of meter (ie smart vs dumb) can change the tariff behaviour, so we # have to enumerate the different meters being used for each tariff as well. for meter in point["meters"]: - is_smart_meter = meter["is_smart_meter"] - if active_tariff_code is not None: - key = (point["mpan"], is_smart_meter) - if key not in tariff_codes: - tariff_codes[(point["mpan"], is_smart_meter)] = active_tariff_code - - return tariff_codes + if active_tariff_code is not None and point["mpan"] == target_mpan and meter["serial_number"] == target_serial_number: + return active_tariff_code + + return None -def get_current_gas_agreement_tariff_codes(current: datetime, account_info): - tariff_codes = {} - if account_info is not None and len(account_info["gas_meter_points"]) > 0: +def get_gas_meter_tariff_code(current: datetime, account_info, target_mprn: str, target_serial_number: str): + if len(account_info["gas_meter_points"]) > 0: for point in account_info["gas_meter_points"]: active_tariff_code = get_active_tariff_code(current, point["agreements"]) - # The type of meter (ie smart vs dumb) can change the tariff behaviour, so we - # have to enumerate the different meters being used for each tariff as well. for meter in point["meters"]: - is_smart_meter = meter["is_smart_meter"] - if active_tariff_code is not None: - key = (point["mprn"], is_smart_meter) - 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 + if active_tariff_code is not None and point["mprn"] == target_mprn and meter["serial_number"] == target_serial_number: + return active_tariff_code + + return None \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/account.py b/custom_components/octopus_energy/coordinators/account.py index cc02e62c..d1e46c39 100644 --- a/custom_components/octopus_energy/coordinators/account.py +++ b/custom_components/octopus_energy/coordinators/account.py @@ -12,6 +12,7 @@ from homeassistant.helpers import issue_registry as ir from ..const import ( + COORDINATOR_REFRESH_IN_SECONDS, DOMAIN, DATA_CLIENT, @@ -74,7 +75,8 @@ async def async_update_account_data(): update_method=async_update_account_data, # Because of how we're using the data, we'll update every minute, but we will only actually retrieve # data every 30 minutes - update_interval=timedelta(minutes=1), + update_interval=timedelta(minutes=COORDINATOR_REFRESH_IN_SECONDS), + always_update=True ) await hass.data[DOMAIN][DATA_ACCOUNT_COORDINATOR].async_config_entry_first_refresh() \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/current_consumption.py b/custom_components/octopus_energy/coordinators/current_consumption.py index b3da7721..11bbd14b 100644 --- a/custom_components/octopus_energy/coordinators/current_consumption.py +++ b/custom_components/octopus_energy/coordinators/current_consumption.py @@ -42,6 +42,7 @@ async def async_update_data(): name=f"current_consumption_{device_id}", update_method=async_update_data, update_interval=timedelta(minutes=refresh_rate_in_minutes), + always_update=True ) await coordinator.async_config_entry_first_refresh() diff --git a/custom_components/octopus_energy/coordinators/electricity_rates.py b/custom_components/octopus_energy/coordinators/electricity_rates.py index c79b99e3..707a862a 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 ( @@ -7,71 +8,94 @@ ) from ..const import ( + COORDINATOR_REFRESH_IN_SECONDS, DOMAIN, DATA_CLIENT, - DATA_ELECTRICITY_RATES_COORDINATOR, - DATA_ELECTRICITY_RATES, + DATA_ELECTRICITY_RATES_COORDINATOR_KEY, + DATA_ELECTRICITY_RATES_KEY, 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_electricity_meter_tariff_code, raise_rate_events from ..intelligent import adjust_intelligent_rates _LOGGER = logging.getLogger(__name__) +class ElectricityRatesCoordinatorResult: + last_retrieved: datetime + rates: list + + def __init__(self, last_retrieved: datetime, rates: list): + self.last_retrieved = last_retrieved + self.rates = rates + async def async_refresh_electricity_rates_data( current: datetime, client: OctopusEnergyApiClient, account_info, - existing_rates: list, - dispatches: list - ): + target_mpan: str, + target_serial_number: str, + is_smart_meter: bool, + is_export_meter: bool, + existing_rates_result: ElectricityRatesCoordinatorResult, + dispatches: list, + fire_event: Callable[[str, "dict[str, Any]"], None], + ) -> ElectricityRatesCoordinatorResult: if (account_info is not None): - tariff_codes = get_current_electricity_agreement_tariff_codes(current, account_info) - period_from = as_utc((current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)) period_to = as_utc((current + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)) - rates = {} - for ((meter_point, is_smart_meter), tariff_code) in tariff_codes.items(): - key = meter_point - - new_rates = None - if ((current.minute % 30) == 0 or - existing_rates is None or - key not in existing_rates or - existing_rates[key][-1]["valid_from"] < period_from): - try: - new_rates = await client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to) - _LOGGER.debug(f'Electricity rates retrieved for {tariff_code}') - except: - _LOGGER.debug('Failed to retrieve electricity rates') - else: - new_rates = existing_rates[key] + tariff_code = get_electricity_meter_tariff_code(current, account_info, target_mpan, target_serial_number) + if tariff_code is None: + return None + + new_rates: list = None + if ((current.minute % 30) == 0 or + existing_rates_result is None or + existing_rates_result.rates is None or + len(existing_rates_result.rates) < 1 or + existing_rates_result.rates[-1]["valid_from"] < period_from): + try: + new_rates = await client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to) + except: + _LOGGER.debug(f'Failed to retrieve electricity rates for {target_mpan}/{target_serial_number} ({tariff_code})') + + if new_rates is not None: + _LOGGER.debug(f'Electricity rates retrieved for {target_mpan}/{target_serial_number} ({tariff_code});') + + if dispatches is not None and is_export_meter == False: + new_rates = adjust_intelligent_rates(new_rates, + dispatches["planned"] if "planned" in dispatches else [], + dispatches["completed"] if "completed" in dispatches else []) - if new_rates is not None: - if dispatches is not None: - rates[key] = adjust_intelligent_rates(new_rates, - dispatches["planned"] if "planned" in dispatches else [], - dispatches["completed"] if "completed" in dispatches else []) - - _LOGGER.debug(f"Rates adjusted: {rates[key]}; dispatches: {dispatches}") - else: - rates[key] = new_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] - - return rates + _LOGGER.debug(f"Rates adjusted: {new_rates}; dispatches: {dispatches}") + + raise_rate_events(current, + new_rates, + { "mpan": target_mpan, "serial_number": target_serial_number, "tariff_code": tariff_code }, + fire_event, + EVENT_ELECTRICITY_PREVIOUS_DAY_RATES, + EVENT_ELECTRICITY_CURRENT_DAY_RATES, + EVENT_ELECTRICITY_NEXT_DAY_RATES) + + return ElectricityRatesCoordinatorResult(current, new_rates) + + elif (existing_rates_result is not None): + _LOGGER.debug(f"Failed to retrieve new electricity rates for {target_mpan}/{target_serial_number}, so using cached rates") - return existing_rates + return existing_rates_result + +async def async_setup_electricity_rates_coordinator(hass, target_mpan: str, target_serial_number: str, is_smart_meter: bool, is_export_meter: bool): + key = DATA_ELECTRICITY_RATES_KEY.format(target_mpan, target_serial_number) -async def async_setup_electricity_rates_coordinator(hass, account_id: str): # Reset data rates as we might have new information - hass.data[DOMAIN][DATA_ELECTRICITY_RATES] = [] + hass.data[DOMAIN][key] = None async def async_update_electricity_rates_data(): """Fetch data from API endpoint.""" @@ -79,26 +103,33 @@ async def async_update_electricity_rates_data(): client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] account_info = hass.data[DOMAIN][DATA_ACCOUNT] if DATA_ACCOUNT in hass.data[DOMAIN] else None dispatches = hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES] if DATA_INTELLIGENT_DISPATCHES in hass.data[DOMAIN] else None - rates = hass.data[DOMAIN][DATA_ELECTRICITY_RATES] if DATA_ELECTRICITY_RATES in hass.data[DOMAIN] else {} + rates = hass.data[DOMAIN][key] if key in hass.data[DOMAIN] else None - hass.data[DOMAIN][DATA_ELECTRICITY_RATES] = await async_refresh_electricity_rates_data( + hass.data[DOMAIN][key] = await async_refresh_electricity_rates_data( current, client, account_info, + target_mpan, + target_serial_number, + is_smart_meter, + is_export_meter, rates, - dispatches + dispatches, + hass.bus.async_fire ) - return hass.data[DOMAIN][DATA_ELECTRICITY_RATES] + return hass.data[DOMAIN][key] - hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] = DataUpdateCoordinator( + coordinator_key = DATA_ELECTRICITY_RATES_COORDINATOR_KEY.format(target_mpan, target_serial_number) + hass.data[DOMAIN][coordinator_key] = DataUpdateCoordinator( hass, _LOGGER, - name="electricity_rates", + name=key, update_method=async_update_electricity_rates_data, # Because of how we're using the data, we'll update every minute, but we will only actually retrieve # data every 30 minutes - update_interval=timedelta(minutes=1), + update_interval=timedelta(seconds=COORDINATOR_REFRESH_IN_SECONDS), + always_update=True ) - await hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR].async_config_entry_first_refresh() \ No newline at end of file + await hass.data[DOMAIN][coordinator_key].async_config_entry_first_refresh() \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/electricity_standing_charges.py b/custom_components/octopus_energy/coordinators/electricity_standing_charges.py index 2fcafc22..b0722c95 100644 --- a/custom_components/octopus_energy/coordinators/electricity_standing_charges.py +++ b/custom_components/octopus_energy/coordinators/electricity_standing_charges.py @@ -1,5 +1,6 @@ import logging from datetime import datetime, timedelta +from custom_components.octopus_energy.coordinators import get_electricity_meter_tariff_code from homeassistant.util.dt import (now, as_utc) from homeassistant.helpers.update_coordinator import ( @@ -7,85 +8,92 @@ ) from ..const import ( + COORDINATOR_REFRESH_IN_SECONDS, + DATA_ELECTRICITY_STANDING_CHARGE_KEY, DOMAIN, DATA_CLIENT, - DATA_ELECTRICITY_STANDING_CHARGES, DATA_ACCOUNT, ) from ..api_client import OctopusEnergyApiClient -from . import get_current_electricity_agreement_tariff_codes - _LOGGER = logging.getLogger(__name__) +class ElectricityStandingChargeCoordinatorResult: + last_retrieved: datetime + standing_charge: {} + + def __init__(self, last_retrieved: datetime, standing_charge: {}): + self.last_retrieved = last_retrieved + self.standing_charge = standing_charge + async def async_refresh_electricity_standing_charges_data( current: datetime, client: OctopusEnergyApiClient, account_info, - existing_standing_charges: list + target_mpan: str, + target_serial_number: str, + existing_standing_charges_result: ElectricityStandingChargeCoordinatorResult ): - if (account_info is not None): - tariff_codes = get_current_electricity_agreement_tariff_codes(current, account_info) - - period_from = as_utc(current.replace(hour=0, minute=0, second=0, microsecond=0)) - period_to = period_from + timedelta(days=1) + period_from = as_utc(current.replace(hour=0, minute=0, second=0, microsecond=0)) + period_to = period_from + timedelta(days=1) - standing_charges = {} - for ((meter_point, is_smart_meter), tariff_code) in tariff_codes.items(): - key = meter_point - - new_standing_charges = None - if ((current.minute % 30) == 0 or - existing_standing_charges is None or - key not in existing_standing_charges or - (existing_standing_charges[key]["valid_from"] is not None and existing_standing_charges[key]["valid_from"] < period_from)): - try: - new_standing_charges = await client.async_get_electricity_standing_charge(tariff_code, period_from, period_to) - _LOGGER.debug(f'Electricity standing charges retrieved for {tariff_code}') - except: - _LOGGER.debug(f'Failed to retrieve electricity standing charges for {tariff_code}') - else: - new_standing_charges = existing_standing_charges[key] - - if new_standing_charges is not None: - standing_charges[key] = new_standing_charges - elif (existing_standing_charges is not None and key in existing_standing_charges): - _LOGGER.debug(f"Failed to retrieve new electricity standing charges for {tariff_code}, so using cached standing charges") - standing_charges[key] = existing_standing_charges[key] - - return standing_charges + if (account_info is not None): + tariff_code = get_electricity_meter_tariff_code(current, account_info, target_mpan, target_serial_number) + if tariff_code is None: + return None + + new_standing_charge = None + if ((current.minute % 30) == 0 or + existing_standing_charges_result is None or + existing_standing_charges_result.standing_charge is None or + (existing_standing_charges_result.standing_charge["valid_from"] is not None and existing_standing_charges_result.standing_charge["valid_from"] < period_from)): + try: + new_standing_charge = await client.async_get_electricity_standing_charge(tariff_code, period_from, period_to) + _LOGGER.debug(f'Electricity standing charges retrieved for {target_mpan}/{target_serial_number} ({tariff_code})') + except: + _LOGGER.debug(f'Failed to retrieve electricity standing charges for {target_mpan}/{target_serial_number} ({tariff_code})') + + if new_standing_charge is not None: + return ElectricityStandingChargeCoordinatorResult(current, new_standing_charge) + elif (existing_standing_charges_result is not None): + _LOGGER.debug(f"Failed to retrieve new electricity standing charges for {target_mpan}/{target_serial_number} ({tariff_code}), so using cached standing charges") - return existing_standing_charges + return existing_standing_charges_result -async def async_setup_electricity_standing_charges_coordinator(hass, account_id: str): +async def async_setup_electricity_standing_charges_coordinator(hass, target_mpan: str, target_serial_number: str): + key = DATA_ELECTRICITY_STANDING_CHARGE_KEY.format(target_mpan, target_serial_number) + # Reset data rates as we might have new information - hass.data[DOMAIN][DATA_ELECTRICITY_STANDING_CHARGES] = [] + hass.data[DOMAIN][key] = None async def async_update_electricity_standing_charges_data(): """Fetch data from API endpoint.""" current = now() client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] account_info = hass.data[DOMAIN][DATA_ACCOUNT] if DATA_ACCOUNT in hass.data[DOMAIN] else None - standing_charges = hass.data[DOMAIN][DATA_ELECTRICITY_STANDING_CHARGES] if DATA_ELECTRICITY_STANDING_CHARGES in hass.data[DOMAIN] else {} + standing_charges: ElectricityStandingChargeCoordinatorResult = hass.data[DOMAIN][key] if key in hass.data[DOMAIN] else None - hass.data[DOMAIN][DATA_ELECTRICITY_STANDING_CHARGES] = await async_refresh_electricity_standing_charges_data( + hass.data[DOMAIN][key] = await async_refresh_electricity_standing_charges_data( current, client, account_info, + target_mpan, + target_serial_number, standing_charges, ) - return hass.data[DOMAIN][DATA_ELECTRICITY_STANDING_CHARGES] + return hass.data[DOMAIN][key] coordinator = DataUpdateCoordinator( hass, _LOGGER, - name="electricity_standing_charges", + name=key, update_method=async_update_electricity_standing_charges_data, # Because of how we're using the data, we'll update every minute, but we will only actually retrieve # data every 30 minutes - update_interval=timedelta(minutes=1), + update_interval=timedelta(minutes=COORDINATOR_REFRESH_IN_SECONDS), + always_update=True ) await coordinator.async_config_entry_first_refresh() diff --git a/custom_components/octopus_energy/coordinators/gas_rates.py b/custom_components/octopus_energy/coordinators/gas_rates.py index d978c82f..56863a03 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 datetime import datetime, timedelta +from typing import Callable, Any from homeassistant.util.dt import (now, as_utc) from homeassistant.helpers.update_coordinator import ( @@ -7,84 +8,111 @@ ) from ..const import ( - DATA_ACCOUNT, + COORDINATOR_REFRESH_IN_SECONDS, DOMAIN, - DATA_GAS_RATES + DATA_GAS_RATES_KEY, + DATA_ACCOUNT, + EVENT_GAS_CURRENT_DAY_RATES, + EVENT_GAS_NEXT_DAY_RATES, + EVENT_GAS_PREVIOUS_DAY_RATES, ) -from ..api_client import (OctopusEnergyApiClient) +from ..api_client import OctopusEnergyApiClient -from . import get_current_gas_agreement_tariff_codes +from . import get_gas_meter_tariff_code, raise_rate_events _LOGGER = logging.getLogger(__name__) +class GasRatesCoordinatorResult: + last_retrieved: datetime + rates: list + + def __init__(self, last_retrieved: datetime, rates: list): + self.last_retrieved = last_retrieved + self.rates = rates + async def async_refresh_gas_rates_data( current: datetime, client: OctopusEnergyApiClient, account_info, - existing_rates: list - ): + target_mprn: str, + target_serial_number: str, + existing_rates_result: GasRatesCoordinatorResult, + fire_event: Callable[[str, "dict[str, Any]"], None], + ) -> GasRatesCoordinatorResult: if (account_info is not None): - tariff_codes = get_current_gas_agreement_tariff_codes(current, account_info) - period_from = as_utc((current - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)) period_to = as_utc((current + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)) - rates = {} - for ((meter_point, is_smart_meter), tariff_code) in tariff_codes.items(): - key = meter_point - - new_rates = None - if ((current.minute % 30) == 0 or - existing_rates is None or - key not in existing_rates or - existing_rates[key][-1]["valid_from"] < period_from): - try: - new_rates = await client.async_get_gas_rates(tariff_code, period_from, period_to) - _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 - 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] - - return rates + tariff_code = get_gas_meter_tariff_code(current, account_info, target_mprn, target_serial_number) + if tariff_code is None: + return None + + new_rates: list = None + if ((current.minute % 30) == 0 or + existing_rates_result is None or + existing_rates_result.rates is None or + len(existing_rates_result.rates) < 1 or + existing_rates_result.rates[-1]["valid_from"] < period_from): + try: + new_rates = await client.async_get_gas_rates(tariff_code, period_from, period_to) + except: + _LOGGER.debug(f'Failed to retrieve gas rates for {target_mprn}/{target_serial_number} ({tariff_code})') + + if new_rates is not None: + _LOGGER.debug(f'Gas rates retrieved for {target_mprn}/{target_serial_number} ({tariff_code});') + + raise_rate_events(current, + new_rates, + { "mprn": target_mprn, "serial_number": target_serial_number, "tariff_code": tariff_code }, + fire_event, + EVENT_GAS_PREVIOUS_DAY_RATES, + EVENT_GAS_CURRENT_DAY_RATES, + EVENT_GAS_NEXT_DAY_RATES) + + return GasRatesCoordinatorResult(current, new_rates) + + elif (existing_rates_result is not None): + _LOGGER.debug(f"Failed to retrieve new gas rates for {target_mprn}/{target_serial_number}, so using cached rates") + return existing_rates_result - return existing_rates + return existing_rates_result -async def async_create_gas_rate_coordinator(hass, client: OctopusEnergyApiClient): - """Create gas rate coordinator""" - # Reset data rates as we might have new information - hass.data[DOMAIN][DATA_GAS_RATES] = [] +async def async_setup_gas_rates_coordinator(hass, client: OctopusEnergyApiClient, target_mprn: str, target_serial_number: str): + key = DATA_GAS_RATES_KEY.format(target_mprn, target_serial_number) - async def async_update_data(): + # Reset data rates as we might have new information + hass.data[DOMAIN][key] = None + + async def async_update_gas_rates_data(): """Fetch data from API endpoint.""" current = now() account_info = hass.data[DOMAIN][DATA_ACCOUNT] if DATA_ACCOUNT in hass.data[DOMAIN] else None - rates = hass.data[DOMAIN][DATA_GAS_RATES] if DATA_GAS_RATES in hass.data[DOMAIN] else {} + rates = hass.data[DOMAIN][key] if key in hass.data[DOMAIN] else None - hass.data[DOMAIN][DATA_GAS_RATES] = await async_refresh_gas_rates_data( + hass.data[DOMAIN][key] = await async_refresh_gas_rates_data( current, client, account_info, - rates + target_mprn, + target_serial_number, + rates, + hass.bus.async_fire ) - return hass.data[DOMAIN][DATA_GAS_RATES] + return hass.data[DOMAIN][key] coordinator = DataUpdateCoordinator( hass, _LOGGER, - name=f"gas_rates", - update_method=async_update_data, - update_interval=timedelta(minutes=1), + name=key, + update_method=async_update_gas_rates_data, + # Because of how we're using the data, we'll update every minute, but we will only actually retrieve + # data every 30 minutes + update_interval=timedelta(seconds=COORDINATOR_REFRESH_IN_SECONDS), + always_update=True ) - + await coordinator.async_config_entry_first_refresh() return coordinator \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/gas_standing_charges.py b/custom_components/octopus_energy/coordinators/gas_standing_charges.py index fdcbefb1..f7684dd5 100644 --- a/custom_components/octopus_energy/coordinators/gas_standing_charges.py +++ b/custom_components/octopus_energy/coordinators/gas_standing_charges.py @@ -1,5 +1,6 @@ import logging from datetime import datetime, timedelta +from custom_components.octopus_energy.coordinators import get_gas_meter_tariff_code from homeassistant.util.dt import (now, as_utc) from homeassistant.helpers.update_coordinator import ( @@ -7,85 +8,92 @@ ) from ..const import ( + COORDINATOR_REFRESH_IN_SECONDS, + DATA_GAS_STANDING_CHARGE_KEY, DOMAIN, DATA_CLIENT, - DATA_GAS_STANDING_CHARGES, DATA_ACCOUNT, ) from ..api_client import OctopusEnergyApiClient -from . import get_current_gas_agreement_tariff_codes - _LOGGER = logging.getLogger(__name__) +class GasStandingChargeCoordinatorResult: + last_retrieved: datetime + standing_charge: {} + + def __init__(self, last_retrieved: datetime, standing_charge: {}): + self.last_retrieved = last_retrieved + self.standing_charge = standing_charge + async def async_refresh_gas_standing_charges_data( current: datetime, client: OctopusEnergyApiClient, account_info, - existing_standing_charges: list + target_mprn: str, + target_serial_number: str, + existing_standing_charges_result: GasStandingChargeCoordinatorResult ): - if (account_info is not None): - tariff_codes = get_current_gas_agreement_tariff_codes(current, account_info) - - period_from = as_utc(current.replace(hour=0, minute=0, second=0, microsecond=0)) - period_to = period_from + timedelta(days=1) + period_from = as_utc(current.replace(hour=0, minute=0, second=0, microsecond=0)) + period_to = period_from + timedelta(days=1) - standing_charges = {} - for ((meter_point, is_smart_meter), tariff_code) in tariff_codes.items(): - key = meter_point - - new_standing_charges = None - if ((current.minute % 30) == 0 or - existing_standing_charges is None or - key not in existing_standing_charges or - (existing_standing_charges[key]["valid_from"] is not None and existing_standing_charges[key]["valid_from"] < period_from)): - try: - new_standing_charges = await client.async_get_gas_standing_charge(tariff_code, period_from, period_to) - _LOGGER.debug(f'Gas standing charges retrieved for {tariff_code}') - except: - _LOGGER.debug(f'Failed to retrieve gas standing charges for {tariff_code}') - else: - new_standing_charges = existing_standing_charges[key] - - if new_standing_charges is not None: - standing_charges[key] = new_standing_charges - elif (existing_standing_charges is not None and key in existing_standing_charges): - _LOGGER.debug(f"Failed to retrieve new gas standing charges for {tariff_code}, so using cached standing charges") - standing_charges[key] = existing_standing_charges[key] - - return standing_charges + if (account_info is not None): + tariff_code = get_gas_meter_tariff_code(current, account_info, target_mprn, target_serial_number) + if tariff_code is None: + return None + + new_standing_charge = None + if ((current.minute % 30) == 0 or + existing_standing_charges_result is None or + existing_standing_charges_result.standing_charge is None or + (existing_standing_charges_result.standing_charge["valid_from"] is not None and existing_standing_charges_result.standing_charge["valid_from"] < period_from)): + try: + new_standing_charge = await client.async_get_gas_standing_charge(tariff_code, period_from, period_to) + _LOGGER.debug(f'Gas standing charges retrieved for {target_mprn}/{target_serial_number} ({tariff_code})') + except: + _LOGGER.debug(f'Failed to retrieve gas standing charges for {target_mprn}/{target_serial_number} ({tariff_code})') + + if new_standing_charge is not None: + return GasStandingChargeCoordinatorResult(current, new_standing_charge) + elif (existing_standing_charges_result is not None): + _LOGGER.debug(f"Failed to retrieve new gas standing charges for {target_mprn}/{target_serial_number} ({tariff_code}), so using cached standing charges") - return existing_standing_charges + return existing_standing_charges_result -async def async_setup_gas_standing_charges_coordinator(hass, account_id: str): +async def async_setup_gas_standing_charges_coordinator(hass, target_mprn: str, target_serial_number: str): + key = DATA_GAS_STANDING_CHARGE_KEY.format(target_mprn, target_serial_number) + # Reset data rates as we might have new information - hass.data[DOMAIN][DATA_GAS_STANDING_CHARGES] = [] + hass.data[DOMAIN][key] = None async def async_update_gas_standing_charges_data(): """Fetch data from API endpoint.""" current = now() client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] account_info = hass.data[DOMAIN][DATA_ACCOUNT] if DATA_ACCOUNT in hass.data[DOMAIN] else None - standing_charges = hass.data[DOMAIN][DATA_GAS_STANDING_CHARGES] if DATA_GAS_STANDING_CHARGES in hass.data[DOMAIN] else {} + standing_charges: GasStandingChargeCoordinatorResult = hass.data[DOMAIN][key] if key in hass.data[DOMAIN] else None - hass.data[DOMAIN][DATA_GAS_STANDING_CHARGES] = await async_refresh_gas_standing_charges_data( + hass.data[DOMAIN][key] = await async_refresh_gas_standing_charges_data( current, client, account_info, + target_mprn, + target_serial_number, standing_charges, ) - return hass.data[DOMAIN][DATA_GAS_STANDING_CHARGES] + return hass.data[DOMAIN][key] coordinator = DataUpdateCoordinator( hass, _LOGGER, - name="gas_standing_charges", + name=key, update_method=async_update_gas_standing_charges_data, # Because of how we're using the data, we'll update every minute, but we will only actually retrieve # data every 30 minutes - update_interval=timedelta(minutes=1), + update_interval=timedelta(minutes=COORDINATOR_REFRESH_IN_SECONDS), + always_update=True ) await coordinator.async_config_entry_first_refresh() diff --git a/custom_components/octopus_energy/coordinators/intelligent_dispatches.py b/custom_components/octopus_energy/coordinators/intelligent_dispatches.py index be4adfa7..387e2b09 100644 --- a/custom_components/octopus_energy/coordinators/intelligent_dispatches.py +++ b/custom_components/octopus_energy/coordinators/intelligent_dispatches.py @@ -1,8 +1,7 @@ import logging from datetime import timedelta -from ..coordinators import get_current_electricity_agreement_tariff_codes -from ..intelligent import async_mock_intelligent_data, clean_previous_dispatches, is_intelligent_tariff, mock_intelligent_dispatches +from ..intelligent import async_mock_intelligent_data, clean_previous_dispatches, has_intelligent_tariff, mock_intelligent_dispatches from homeassistant.util.dt import (utcnow) from homeassistant.helpers.update_coordinator import ( @@ -11,6 +10,7 @@ from homeassistant.helpers import storage from ..const import ( + COORDINATOR_REFRESH_IN_SECONDS, DOMAIN, DATA_CLIENT, @@ -53,17 +53,13 @@ async def async_update_intelligent_dispatches_data(): client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] if (DATA_ACCOUNT in hass.data[DOMAIN]): - tariff_codes = get_current_electricity_agreement_tariff_codes(current, hass.data[DOMAIN][DATA_ACCOUNT]) - dispatches = None - for ((meter_point), tariff_code) in tariff_codes.items(): - if is_intelligent_tariff(tariff_code): - try: - dispatches = await client.async_get_intelligent_dispatches(account_id) - _LOGGER.debug(f'Intelligent dispatches retrieved for {tariff_code}') - except: - _LOGGER.debug('Failed to retrieve intelligent dispatches') - break + if has_intelligent_tariff(current, hass.data[DOMAIN][DATA_ACCOUNT]): + try: + dispatches = await client.async_get_intelligent_dispatches(account_id) + _LOGGER.debug(f'Intelligent dispatches retrieved for account {account_id}') + except: + _LOGGER.debug('Failed to retrieve intelligent dispatches for account {account_id}') if await async_mock_intelligent_data(hass): dispatches = mock_intelligent_dispatches() @@ -84,7 +80,8 @@ async def async_update_intelligent_dispatches_data(): update_method=async_update_intelligent_dispatches_data, # Because of how we're using the data, we'll update every minute, but we will only actually retrieve # data every 30 minutes - update_interval=timedelta(minutes=1), + update_interval=timedelta(minutes=COORDINATOR_REFRESH_IN_SECONDS), + always_update=True ) await hass.data[DOMAIN][DATA_INTELLIGENT_DISPATCHES_COORDINATOR].async_config_entry_first_refresh() \ No newline at end of file diff --git a/custom_components/octopus_energy/coordinators/intelligent_settings.py b/custom_components/octopus_energy/coordinators/intelligent_settings.py index b324c130..21b82a3d 100644 --- a/custom_components/octopus_energy/coordinators/intelligent_settings.py +++ b/custom_components/octopus_energy/coordinators/intelligent_settings.py @@ -1,8 +1,7 @@ import logging from datetime import timedelta -from . import get_current_electricity_agreement_tariff_codes -from ..intelligent import async_mock_intelligent_data, clean_previous_dispatches, is_intelligent_tariff, mock_intelligent_settings +from ..intelligent import async_mock_intelligent_data, clean_previous_dispatches, has_intelligent_tariff, mock_intelligent_settings from homeassistant.util.dt import (utcnow) from homeassistant.helpers.update_coordinator import ( @@ -11,6 +10,7 @@ from homeassistant.helpers import storage from ..const import ( + COORDINATOR_REFRESH_IN_SECONDS, DOMAIN, DATA_CLIENT, @@ -39,18 +39,13 @@ async def async_update_intelligent_settings_data(): client: OctopusEnergyApiClient = hass.data[DOMAIN][DATA_CLIENT] if (DATA_ACCOUNT in hass.data[DOMAIN]): - tariff_codes = get_current_electricity_agreement_tariff_codes(current, hass.data[DOMAIN][DATA_ACCOUNT]) - _LOGGER.debug(f'tariff_codes: {tariff_codes}') - settings = None - for ((meter_point), tariff_code) in tariff_codes.items(): - if is_intelligent_tariff(tariff_code): - try: - settings = await client.async_get_intelligent_settings(account_id) - _LOGGER.debug(f'Intelligent settings retrieved for {tariff_code}') - except: - _LOGGER.debug('Failed to retrieve intelligent dispatches') - break + if has_intelligent_tariff(current, hass.data[DOMAIN][DATA_ACCOUNT]): + try: + settings = await client.async_get_intelligent_settings(account_id) + _LOGGER.debug(f'Intelligent settings retrieved for account {account_id}') + except: + _LOGGER.debug('Failed to retrieve intelligent dispatches for account {account_id}') if await async_mock_intelligent_data(hass): settings = mock_intelligent_settings() @@ -68,7 +63,8 @@ async def async_update_intelligent_settings_data(): _LOGGER, name="intelligent_settings", update_method=async_update_intelligent_settings_data, - update_interval=timedelta(minutes=1), + update_interval=timedelta(minutes=COORDINATOR_REFRESH_IN_SECONDS), + always_update=True ) await hass.data[DOMAIN][DATA_INTELLIGENT_SETTINGS_COORDINATOR].async_config_entry_first_refresh() \ No newline at end of file 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..e12906e9 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,7 @@ from datetime import timedelta import logging +from typing import Callable, Any +import asyncio from homeassistant.util.dt import (utcnow, now, as_utc) from homeassistant.helpers.update_coordinator import ( @@ -7,8 +9,12 @@ ) from ..const import ( + COORDINATOR_REFRESH_IN_SECONDS, 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 +42,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,36 +53,50 @@ 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) - rate_data = await client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to) + + [consumption_data, rate_data, standing_charge] = await asyncio.gather( + client.async_get_electricity_consumption(identifier, serial_number, period_from, period_to), + client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to), + client.async_get_electricity_standing_charge(tariff_code, period_from, period_to) + ) + if intelligent_dispatches is not None: rate_data = adjust_intelligent_rates(rate_data, intelligent_dispatches["planned"] if "planned" in intelligent_dispatches else [], intelligent_dispatches["completed"] if "completed" in intelligent_dispatches else []) _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}') + [consumption_data, rate_data, standing_charge] = await asyncio.gather( + client.async_get_gas_consumption(identifier, serial_number, period_from, period_to), + client.async_get_gas_rates(tariff_code, period_from, period_to), + client.async_get_gas_standing_charge(tariff_code, period_from, period_to) + ) - 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, "serial_number": serial_number, "tariff_code": tariff_code, "rates": rate_data }) + else: + fire_event(EVENT_GAS_PREVIOUS_CONSUMPTION_RATES, { "mprn": identifier, "serial_number": serial_number, "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 +133,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 ) @@ -130,7 +152,8 @@ async def async_update_data(): update_method=async_update_data, # Because of how we're using the data, we'll update every minute, but we will only actually retrieve # data every 30 minutes - update_interval=timedelta(minutes=1), + update_interval=timedelta(minutes=COORDINATOR_REFRESH_IN_SECONDS), + always_update=True ) hass.data[DOMAIN][f'{identifier}_{serial_number}_previous_consumption_and_cost_coordinator'] = coordinator diff --git a/custom_components/octopus_energy/coordinators/saving_sessions.py b/custom_components/octopus_energy/coordinators/saving_sessions.py index 248644ec..be457dc6 100644 --- a/custom_components/octopus_energy/coordinators/saving_sessions.py +++ b/custom_components/octopus_energy/coordinators/saving_sessions.py @@ -7,6 +7,7 @@ ) from ..const import ( + COORDINATOR_REFRESH_IN_SECONDS, DOMAIN, DATA_CLIENT, DATA_ACCOUNT_ID, @@ -41,7 +42,8 @@ async def async_update_saving_sessions(): update_method=async_update_saving_sessions, # Because of how we're using the data, we'll update every minute, but we will only actually retrieve # data every 30 minutes - update_interval=timedelta(minutes=1), + update_interval=timedelta(minutes=COORDINATOR_REFRESH_IN_SECONDS), + always_update=True ) await hass.data[DOMAIN][DATA_SAVING_SESSIONS_COORDINATOR].async_config_entry_first_refresh() \ No newline at end of file diff --git a/custom_components/octopus_energy/electricity/current_accumulative_consumption.py b/custom_components/octopus_energy/electricity/current_accumulative_consumption.py index fe978508..c17874df 100644 --- a/custom_components/octopus_energy/electricity/current_accumulative_consumption.py +++ b/custom_components/octopus_energy/electricity/current_accumulative_consumption.py @@ -6,11 +6,10 @@ CoordinatorEntity ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.const import ( ENERGY_KILO_WATT_HOUR ) @@ -21,12 +20,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyCurrentAccumulativeElectricityConsumption(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyCurrentAccumulativeElectricityConsumption(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the current accumulative electricity consumption.""" def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._state = None @@ -80,9 +79,9 @@ def last_reset(self): def state(self): """Retrieve the current days accumulative consumption""" consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None - rate_data = self._rates_coordinator.data[self._mpan] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mpan in self._rates_coordinator.data else None - standing_charge = self._standing_charge_coordinator.data[self._mpan]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mpan in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mpan] else None - + rate_data = self._rates_coordinator.data.rates if self._rates_coordinator is not None and self._rates_coordinator.data is not None else None + standing_charge = self._standing_charge_coordinator.data.standing_charge["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None else None + consumption_and_cost = calculate_electricity_consumption_and_cost( consumption_data, rate_data, diff --git a/custom_components/octopus_energy/electricity/current_accumulative_consumption_off_peak.py b/custom_components/octopus_energy/electricity/current_accumulative_consumption_off_peak.py index 39833a71..9a13e3e4 100644 --- a/custom_components/octopus_energy/electricity/current_accumulative_consumption_off_peak.py +++ b/custom_components/octopus_energy/electricity/current_accumulative_consumption_off_peak.py @@ -7,11 +7,10 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.const import ( ENERGY_KILO_WATT_HOUR @@ -25,12 +24,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyCurrentAccumulativeElectricityConsumptionOffPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyCurrentAccumulativeElectricityConsumptionOffPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the current days accumulative electricity reading during off peak hours.""" def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._state = None @@ -92,8 +91,8 @@ def last_reset(self): def state(self): """Retrieve the current days accumulative consumption""" consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None - rate_data = self._rates_coordinator.data[self._mpan] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mpan in self._rates_coordinator.data else None - standing_charge = self._standing_charge_coordinator.data[self._mpan]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mpan in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mpan] else None + rate_data = self._rates_coordinator.data.rates if self._rates_coordinator is not None and self._rates_coordinator.data is not None else None + standing_charge = self._standing_charge_coordinator.data.standing_charge["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None else None consumption_and_cost = calculate_electricity_consumption_and_cost( consumption_data, diff --git a/custom_components/octopus_energy/electricity/current_accumulative_consumption_peak.py b/custom_components/octopus_energy/electricity/current_accumulative_consumption_peak.py index 5425cf2f..d3a495b0 100644 --- a/custom_components/octopus_energy/electricity/current_accumulative_consumption_peak.py +++ b/custom_components/octopus_energy/electricity/current_accumulative_consumption_peak.py @@ -7,11 +7,10 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.const import ( ENERGY_KILO_WATT_HOUR ) @@ -24,12 +23,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyCurrentAccumulativeElectricityConsumptionPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyCurrentAccumulativeElectricityConsumptionPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the current days accumulative electricity reading during peak hours.""" def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._state = None @@ -91,9 +90,9 @@ def last_reset(self): def state(self): """Retrieve the current days accumulative consumption""" consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None - rate_data = self._rates_coordinator.data[self._mpan] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mpan in self._rates_coordinator.data else None - standing_charge = self._standing_charge_coordinator.data[self._mpan]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mpan in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mpan] else None - + rate_data = self._rates_coordinator.data.rates if self._rates_coordinator is not None and self._rates_coordinator.data is not None else None + standing_charge = self._standing_charge_coordinator.data.standing_charge["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None else None + consumption_and_cost = calculate_electricity_consumption_and_cost( consumption_data, rate_data, diff --git a/custom_components/octopus_energy/electricity/current_accumulative_cost.py b/custom_components/octopus_energy/electricity/current_accumulative_cost.py index b95185aa..e5799a54 100644 --- a/custom_components/octopus_energy/electricity/current_accumulative_cost.py +++ b/custom_components/octopus_energy/electricity/current_accumulative_cost.py @@ -7,11 +7,10 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from . import ( calculate_electricity_consumption_and_cost, ) @@ -20,12 +19,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyCurrentAccumulativeElectricityCost(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyCurrentAccumulativeElectricityCost(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the current days accumulative electricity cost.""" def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._hass = hass @@ -88,9 +87,9 @@ def last_reset(self): def state(self): """Retrieve the currently calculated state""" consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None - rate_data = self._rates_coordinator.data[self._mpan] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mpan in self._rates_coordinator.data else None - standing_charge = self._standing_charge_coordinator.data[self._mpan]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mpan in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mpan] else None - + rate_data = self._rates_coordinator.data.rates if self._rates_coordinator is not None and self._rates_coordinator.data is not None else None + standing_charge = self._standing_charge_coordinator.data.standing_charge["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None else None + consumption_and_cost = calculate_electricity_consumption_and_cost( consumption_data, rate_data, diff --git a/custom_components/octopus_energy/electricity/current_accumulative_cost_off_peak.py b/custom_components/octopus_energy/electricity/current_accumulative_cost_off_peak.py index 6c65bcb2..7098933a 100644 --- a/custom_components/octopus_energy/electricity/current_accumulative_cost_off_peak.py +++ b/custom_components/octopus_energy/electricity/current_accumulative_cost_off_peak.py @@ -7,11 +7,10 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from . import ( calculate_electricity_consumption_and_cost, ) @@ -20,12 +19,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyCurrentAccumulativeElectricityCostOffPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyCurrentAccumulativeElectricityCostOffPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the current days accumulative electricity cost during off peak hours.""" def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._state = None @@ -87,9 +86,9 @@ def last_reset(self): def state(self): """Retrieve the currently calculated state""" consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None - rate_data = self._rates_coordinator.data[self._mpan] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mpan in self._rates_coordinator.data else None - standing_charge = self._standing_charge_coordinator.data[self._mpan]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mpan in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mpan] else None - + rate_data = self._rates_coordinator.data.rates if self._rates_coordinator is not None and self._rates_coordinator.data is not None else None + standing_charge = self._standing_charge_coordinator.data.standing_charge["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None else None + consumption_and_cost = calculate_electricity_consumption_and_cost( consumption_data, rate_data, diff --git a/custom_components/octopus_energy/electricity/current_accumulative_cost_peak.py b/custom_components/octopus_energy/electricity/current_accumulative_cost_peak.py index c1c38ccc..6b1e0dcf 100644 --- a/custom_components/octopus_energy/electricity/current_accumulative_cost_peak.py +++ b/custom_components/octopus_energy/electricity/current_accumulative_cost_peak.py @@ -7,11 +7,10 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from . import ( calculate_electricity_consumption_and_cost, ) @@ -20,12 +19,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyCurrentAccumulativeElectricityCostPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyCurrentAccumulativeElectricityCostPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the current days accumulative electricity cost during peak hours.""" def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._state = None @@ -87,9 +86,9 @@ def last_reset(self): def state(self): """Retrieve the currently calculated state""" consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None - rate_data = self._rates_coordinator.data[self._mpan] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mpan in self._rates_coordinator.data else None - standing_charge = self._standing_charge_coordinator.data[self._mpan]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mpan in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mpan] else None - + rate_data = self._rates_coordinator.data.rates if self._rates_coordinator is not None and self._rates_coordinator.data is not None else None + standing_charge = self._standing_charge_coordinator.data.standing_charge["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None else None + consumption_and_cost = calculate_electricity_consumption_and_cost( consumption_data, rate_data, diff --git a/custom_components/octopus_energy/electricity/current_consumption.py b/custom_components/octopus_energy/electricity/current_consumption.py index 6cc827a4..dd96fb18 100644 --- a/custom_components/octopus_energy/electricity/current_consumption.py +++ b/custom_components/octopus_energy/electricity/current_consumption.py @@ -7,11 +7,10 @@ CoordinatorEntity ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.const import ( ENERGY_KILO_WATT_HOUR ) @@ -22,12 +21,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyCurrentElectricityConsumption(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyCurrentElectricityConsumption(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the current electricity consumption.""" def __init__(self, hass: HomeAssistant, coordinator, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._state = None diff --git a/custom_components/octopus_energy/electricity/current_demand.py b/custom_components/octopus_energy/electricity/current_demand.py index aece4c01..7aa8c4f6 100644 --- a/custom_components/octopus_energy/electricity/current_demand.py +++ b/custom_components/octopus_energy/electricity/current_demand.py @@ -7,22 +7,21 @@ CoordinatorEntity ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from .base import (OctopusEnergyElectricitySensor) _LOGGER = logging.getLogger(__name__) -class OctopusEnergyCurrentElectricityDemand(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyCurrentElectricityDemand(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the current electricity demand.""" def __init__(self, hass: HomeAssistant, coordinator, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._state = None diff --git a/custom_components/octopus_energy/electricity/current_rate.py b/custom_components/octopus_energy/electricity/current_rate.py index 3dd672df..af6a64bf 100644 --- a/custom_components/octopus_energy/electricity/current_rate.py +++ b/custom_components/octopus_energy/electricity/current_rate.py @@ -8,11 +8,10 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from .base import (OctopusEnergyElectricitySensor) @@ -20,13 +19,13 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyElectricityCurrentRate(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyElectricityCurrentRate(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the current rate.""" def __init__(self, hass: HomeAssistant, coordinator, meter, point, tariff_code, electricity_price_cap): """Init sensor.""" # Pass coordinator to base class - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._state = None @@ -91,10 +90,11 @@ def state(self): """Retrieve the current rate for the sensor.""" # Find the current rate. We only need to do this every half an hour current = now() + rates = self.coordinator.data.rates if self.coordinator is not None and self.coordinator.data is not None else None if (self._last_updated is None or self._last_updated < (current - timedelta(minutes=30)) or (current.minute % 30) == 0): _LOGGER.debug(f"Updating OctopusEnergyElectricityCurrentRate for '{self._mpan}/{self._serial_number}'") - rate_information = get_current_rate_information(self.coordinator.data[self._mpan] if self.coordinator is not None and self._mpan in self.coordinator.data else None, current) + rate_information = get_current_rate_information(rates, current) if rate_information is not None: self._attributes = { diff --git a/custom_components/octopus_energy/electricity/next_rate.py b/custom_components/octopus_energy/electricity/next_rate.py index 92788021..aaec91c9 100644 --- a/custom_components/octopus_energy/electricity/next_rate.py +++ b/custom_components/octopus_energy/electricity/next_rate.py @@ -8,24 +8,23 @@ CoordinatorEntity ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from .base import (OctopusEnergyElectricitySensor) from ..utils.rate_information import (get_next_rate_information) _LOGGER = logging.getLogger(__name__) -class OctopusEnergyElectricityNextRate(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyElectricityNextRate(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the next rate.""" def __init__(self, hass: HomeAssistant, coordinator, meter, point): """Init sensor.""" # Pass coordinator to base class - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._state = None @@ -81,11 +80,12 @@ def state(self): """Retrieve the next rate for the sensor.""" # Find the next rate. We only need to do this every half an hour current = now() + rates = self.coordinator.data.rates if self.coordinator is not None and self.coordinator.data is not None else None if (self._last_updated is None or self._last_updated < (current - timedelta(minutes=30)) or (current.minute % 30) == 0): _LOGGER.debug(f"Updating OctopusEnergyElectricityNextRate for '{self._mpan}/{self._serial_number}'") target = current - rate_information = get_next_rate_information(self.coordinator.data[self._mpan] if self.coordinator is not None and self._mpan in self.coordinator.data else None, target) + rate_information = get_next_rate_information(rates, target) if rate_information is not None: self._attributes = { diff --git a/custom_components/octopus_energy/electricity/off_peak.py b/custom_components/octopus_energy/electricity/off_peak.py index de0afd6d..b4720765 100644 --- a/custom_components/octopus_energy/electricity/off_peak.py +++ b/custom_components/octopus_energy/electricity/off_peak.py @@ -25,7 +25,7 @@ class OctopusEnergyElectricityOffPeak(CoordinatorEntity, OctopusEnergyElectricit def __init__(self, hass: HomeAssistant, coordinator, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._state = None @@ -58,7 +58,7 @@ def extra_state_attributes(self): def is_on(self): """Determine if current rate is off peak.""" current = now() - rates = self.coordinator.data[self._mpan] if self.coordinator is not None and self._mpan in self.coordinator.data else None + rates = self.coordinator.data.rates if self.coordinator is not None and self.coordinator.data is not None else None if (rates is not None and (self._last_updated is None or self._last_updated < (current - timedelta(minutes=30)) or (current.minute % 30) == 0)): _LOGGER.debug(f"Updating OctopusEnergyElectricityOffPeak for '{self._mpan}/{self._serial_number}'") diff --git a/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py b/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py index 1d5234bf..ac7b9eeb 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_consumption.py @@ -1,19 +1,15 @@ import logging from datetime import datetime -from ..statistics.consumption import async_import_external_statistics_from_consumption - -from homeassistant.core import HomeAssistant -from homeassistant.util.dt import (utcnow) +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.const import ( ENERGY_KILO_WATT_HOUR ) @@ -24,16 +20,21 @@ from .base import (OctopusEnergyElectricitySensor) +from ..statistics.consumption import async_import_external_statistics_from_consumption, get_electricity_consumption_statistic_unique_id +from ..statistics.refresh import async_refresh_previous_electricity_consumption_data +from ..api_client import OctopusEnergyApiClient + _LOGGER = logging.getLogger(__name__) -class OctopusEnergyPreviousAccumulativeElectricityConsumption(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyPreviousAccumulativeElectricityConsumption(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the previous days accumulative electricity reading.""" - def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): + def __init__(self, hass: HomeAssistant, client: OctopusEnergyApiClient, coordinator, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) + self._client = client self._state = None self._tariff_code = tariff_code self._last_reset = None @@ -111,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): @@ -121,7 +120,7 @@ async def async_update(self): await async_import_external_statistics_from_consumption( self._hass, - f"electricity_{self._serial_number}_{self._mpan}{self._export_id_addition}_previous_accumulative_consumption", + get_electricity_consumption_statistic_unique_id(self._serial_number, self._mpan, self._is_export), self.name, consumption_and_cost["charges"], rate_data, @@ -161,4 +160,19 @@ async def async_added_to_hass(self): if x == "last_reset": self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") - _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityConsumption state: {self._state}') \ No newline at end of file + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeElectricityConsumption state: {self._state}') + + @callback + async def async_refresh_previous_consumption_data(self, start_date): + """Update sensors config""" + + await async_refresh_previous_electricity_consumption_data( + self._hass, + self._client, + start_date, + self._mpan, + self._serial_number, + self._tariff_code, + self._is_smart_meter, + self._is_export + ) \ No newline at end of file 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 f83fbd58..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 @@ -7,11 +7,10 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.const import ( ENERGY_KILO_WATT_HOUR ) @@ -24,12 +23,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyPreviousAccumulativeElectricityConsumptionOffPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyPreviousAccumulativeElectricityConsumptionOffPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the previous days accumulative electricity reading during off peak hours.""" def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._state = None @@ -97,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 6c9a6f10..8fd0c69d 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_consumption_peak.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_consumption_peak.py @@ -7,11 +7,10 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.const import ( ENERGY_KILO_WATT_HOUR ) @@ -24,12 +23,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyPreviousAccumulativeElectricityConsumptionPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyPreviousAccumulativeElectricityConsumptionPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the previous days accumulative electricity reading during peak hours.""" def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._state = None @@ -97,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 23d71461..13380d25 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_cost.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost.py @@ -7,27 +7,26 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from . import ( calculate_electricity_consumption_and_cost, ) from .base import (OctopusEnergyElectricitySensor) -from ..statistics.cost import async_import_external_statistics_from_cost +from ..statistics.cost import async_import_external_statistics_from_cost, get_electricity_cost_statistic_unique_id _LOGGER = logging.getLogger(__name__) -class OctopusEnergyPreviousAccumulativeElectricityCost(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyPreviousAccumulativeElectricityCost(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the previous days accumulative electricity cost.""" def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._hass = hass @@ -108,16 +107,14 @@ 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): _LOGGER.debug(f"Calculated previous electricity consumption cost for '{self._mpan}/{self._serial_number}'...") await async_import_external_statistics_from_cost( self._hass, - f"electricity_{self._serial_number}_{self._mpan}_previous_accumulative_cost", + get_electricity_cost_statistic_unique_id(self._serial_number, self._mpan, self._is_export), self.name, consumption_and_cost["charges"], rate_data, 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 99f6bee6..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 @@ -7,11 +7,10 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from . import ( calculate_electricity_consumption_and_cost, ) @@ -20,12 +19,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyPreviousAccumulativeElectricityCostOffPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyPreviousAccumulativeElectricityCostOffPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the previous days accumulative electricity cost during off peak hours.""" def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._hass = hass @@ -94,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 ece90beb..4a719a1e 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_cost_override.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_override.py @@ -1,5 +1,6 @@ import logging from datetime import (datetime) +import asyncio from homeassistant.core import HomeAssistant @@ -7,11 +8,10 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from . import ( calculate_electricity_consumption_and_cost, @@ -21,18 +21,18 @@ 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 _LOGGER = logging.getLogger(__name__) -class OctopusEnergyPreviousAccumulativeElectricityCostOverride(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyPreviousAccumulativeElectricityCostOverride(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the previous days accumulative electricity cost for a different tariff.""" def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._hass = hass @@ -109,27 +109,28 @@ async def async_update(self): tariff_override_key = get_electricity_tariff_override_key(self._serial_number, self._mpan) - is_old_data = self._last_reset is None or self._last_reset < consumption_data[-1]["interval_end"] + is_old_data = self._last_reset is None or (consumption_data is not None and self._last_reset < consumption_data[-1]["interval_end"]) 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] period_from = consumption_data[0]["interval_start"] period_to = consumption_data[-1]["interval_end"] - rate_data = await self._client.async_get_electricity_rates(tariff_override, self._is_smart_meter, period_from, period_to) - standing_charge = await self._client.async_get_electricity_standing_charge(tariff_override, period_from, period_to) + + [rate_data, standing_charge] = await asyncio.gather( + self._client.async_get_electricity_rates(tariff_override, self._is_smart_meter, period_from, period_to), + self._client.async_get_electricity_standing_charge(tariff_override, period_from, period_to) + ) consumption_and_cost = calculate_electricity_consumption_and_cost( consumption_data, 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 @@ -159,6 +160,8 @@ async def async_update(self): }, consumption_and_cost["charges"])) } + self._hass.bus.async_fire(EVENT_ELECTRICITY_PREVIOUS_CONSUMPTION_OVERRIDE_RATES, { "mpan": self._mpan, "serial_number": self._serial_number, "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_override_tariff.py b/custom_components/octopus_energy/electricity/previous_accumulative_cost_override_tariff.py index 9c849ea3..e9cd7002 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_cost_override_tariff.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_override_tariff.py @@ -16,32 +16,18 @@ from ..api_client import OctopusEnergyApiClient +from .base import (OctopusEnergyElectricitySensor) + _LOGGER = logging.getLogger(__name__) -class OctopusEnergyPreviousAccumulativeElectricityCostTariffOverride(TextEntity, RestoreEntity): +class OctopusEnergyPreviousAccumulativeElectricityCostTariffOverride(OctopusEnergyElectricitySensor, TextEntity, RestoreEntity): """Sensor for the tariff for the previous days accumulative electricity cost looking at a different tariff.""" _attr_pattern = REGEX_TARIFF_PARTS def __init__(self, hass: HomeAssistant, client: OctopusEnergyApiClient, tariff_code, meter, point): """Init sensor.""" - - self._point = point - self._meter = meter - - self._mpan = point["mpan"] - self._serial_number = meter["serial_number"] - self._is_export = meter["is_export"] - self._is_smart_meter = meter["is_smart_meter"] - self._export_id_addition = "_export" if self._is_export == True else "" - self._export_name_addition = " Export" if self._is_export == True else "" - - self._attributes = { - "mpan": self._mpan, - "serial_number": self._serial_number, - "is_export": self._is_export, - "is_smart_meter": self._is_smart_meter - } + OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self.entity_id = generate_entity_id("text.{}", self.unique_id, hass=hass) @@ -50,15 +36,6 @@ def __init__(self, hass: HomeAssistant, client: OctopusEnergyApiClient, tariff_c self._client = client self._tariff_code = tariff_code self._attr_native_value = tariff_code - - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, f"electricity_{self._serial_number}_{self._mpan}")}, - name=f"Electricity Meter{self._export_name_addition}", - connections=set(), - manufacturer=self._meter["manufacturer"], - model=self._meter["model"], - sw_version=self._meter["firmware"] - ) @property def entity_registry_enabled_default(self) -> bool: 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 ff88ed4f..4d1966e9 100644 --- a/custom_components/octopus_energy/electricity/previous_accumulative_cost_peak.py +++ b/custom_components/octopus_energy/electricity/previous_accumulative_cost_peak.py @@ -7,10 +7,10 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass,SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from . import ( calculate_electricity_consumption_and_cost, ) @@ -19,12 +19,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyPreviousAccumulativeElectricityCostPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyPreviousAccumulativeElectricityCostPeak(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the previous days accumulative electricity cost during peak hours.""" def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._hass = hass @@ -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_rate.py b/custom_components/octopus_energy/electricity/previous_rate.py index c9694384..da67d78f 100644 --- a/custom_components/octopus_energy/electricity/previous_rate.py +++ b/custom_components/octopus_energy/electricity/previous_rate.py @@ -8,24 +8,23 @@ CoordinatorEntity ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, + SensorStateClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from .base import (OctopusEnergyElectricitySensor) from ..utils.rate_information import (get_previous_rate_information) _LOGGER = logging.getLogger(__name__) -class OctopusEnergyElectricityPreviousRate(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyElectricityPreviousRate(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the previous rate.""" def __init__(self, hass: HomeAssistant, coordinator, meter, point): """Init sensor.""" # Pass coordinator to base class - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyElectricitySensor.__init__(self, hass, meter, point) self._state = None @@ -81,11 +80,12 @@ def state(self): """Retrieve the previous rate.""" # Find the previous rate. We only need to do this every half an hour current = now() + rates = self.coordinator.data.rates if self.coordinator is not None and self.coordinator.data is not None else None if (self._last_updated is None or self._last_updated < (current - timedelta(minutes=30)) or (current.minute % 30) == 0): _LOGGER.debug(f"Updating OctopusEnergyElectricityPreviousRate for '{self._mpan}/{self._serial_number}'") target = current - rate_information = get_previous_rate_information(self.coordinator.data[self._mpan] if self.coordinator is not None and self._mpan in self.coordinator.data else None, target) + rate_information = get_previous_rate_information(rates, target) if rate_information is not None: self._attributes = { 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..ea24af66 --- /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 and "serial_number" in event.data and event.data["serial_number"] == self._serial_number): + 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..feecaab5 --- /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 and "serial_number" in event.data and event.data["serial_number"] == self._serial_number): + 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..8938158b --- /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 and "serial_number" in event.data and event.data["serial_number"] == self._serial_number): + 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..7c50e7cd --- /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 and "serial_number" in event.data and event.data["serial_number"] == self._serial_number): + 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..65b9e002 --- /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 and "serial_number" in event.data and event.data["serial_number"] == self._serial_number): + 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/standing_charge.py b/custom_components/octopus_energy/electricity/standing_charge.py index f074a167..0b4f57ec 100644 --- a/custom_components/octopus_energy/electricity/standing_charge.py +++ b/custom_components/octopus_energy/electricity/standing_charge.py @@ -3,10 +3,9 @@ from homeassistant.core import HomeAssistant from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, + RestoreSensor, + SensorDeviceClass, ) -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, ) @@ -15,7 +14,7 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyElectricityCurrentStandingCharge(CoordinatorEntity, OctopusEnergyElectricitySensor, SensorEntity, RestoreEntity): +class OctopusEnergyElectricityCurrentStandingCharge(CoordinatorEntity, OctopusEnergyElectricitySensor, RestoreSensor): """Sensor for displaying the current standing charge.""" def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): @@ -63,7 +62,7 @@ def state(self): """Retrieve the latest electricity standing charge""" _LOGGER.debug('Updating OctopusEnergyElectricityCurrentStandingCharge') - standard_charge_result = self.coordinator.data[self._mpan] if self.coordinator is not None and self.coordinator.data is not None and self._mpan in self.coordinator.data else None + standard_charge_result = self.coordinator.data.standing_charge if self.coordinator is not None and self.coordinator.data is not None else None if standard_charge_result is not None: self._latest_date = standard_charge_result["valid_from"] 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/base.py b/custom_components/octopus_energy/gas/base.py index 7430d6a6..5466fdf0 100644 --- a/custom_components/octopus_energy/gas/base.py +++ b/custom_components/octopus_energy/gas/base.py @@ -1,17 +1,12 @@ from homeassistant.core import HomeAssistant -from homeassistant.components.sensor import ( - SensorEntity, -) -from homeassistant.helpers.restore_state import RestoreEntity - from homeassistant.helpers.entity import generate_entity_id, DeviceInfo from ..const import ( DOMAIN, ) -class OctopusEnergyGasSensor(SensorEntity, RestoreEntity): +class OctopusEnergyGasSensor: def __init__(self, hass: HomeAssistant, meter, point): """Init sensor""" self._point = point diff --git a/custom_components/octopus_energy/gas/current_accumulative_consumption.py b/custom_components/octopus_energy/gas/current_accumulative_consumption.py index 6c6ebd25..4f4ef616 100644 --- a/custom_components/octopus_energy/gas/current_accumulative_consumption.py +++ b/custom_components/octopus_energy/gas/current_accumulative_consumption.py @@ -6,8 +6,9 @@ CoordinatorEntity ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass + RestoreSensor, + SensorDeviceClass, + SensorStateClass ) from homeassistant.const import ( ENERGY_KILO_WATT_HOUR @@ -19,12 +20,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyCurrentAccumulativeGasConsumption(CoordinatorEntity, OctopusEnergyGasSensor): +class OctopusEnergyCurrentAccumulativeGasConsumption(CoordinatorEntity, OctopusEnergyGasSensor, RestoreSensor): """Sensor for displaying the current accumulative gas consumption.""" def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point, calorific_value): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyGasSensor.__init__(self, hass, meter, point) self._hass = hass @@ -80,9 +81,9 @@ def last_reset(self): def state(self): """Retrieve the current days accumulative consumption""" consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None - rate_data = self._rates_coordinator.data[self._mprn] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mprn in self._rates_coordinator.data else None - standing_charge = self._standing_charge_coordinator.data[self._mprn]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mprn in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mprn] else None - + rate_data = self._rates_coordinator.data.rates if self._rates_coordinator is not None and self._rates_coordinator.data is not None else None + standing_charge = self._standing_charge_coordinator.data.standing_charge["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None else None + consumption_and_cost = calculate_gas_consumption_and_cost( consumption_data, rate_data, diff --git a/custom_components/octopus_energy/gas/current_accumulative_cost.py b/custom_components/octopus_energy/gas/current_accumulative_cost.py index 788e1bf7..00c605dc 100644 --- a/custom_components/octopus_energy/gas/current_accumulative_cost.py +++ b/custom_components/octopus_energy/gas/current_accumulative_cost.py @@ -7,8 +7,9 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass + RestoreSensor, + SensorDeviceClass, + SensorStateClass ) from . import ( calculate_gas_consumption_and_cost, @@ -20,12 +21,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyCurrentAccumulativeGasCost(CoordinatorEntity, OctopusEnergyGasSensor): +class OctopusEnergyCurrentAccumulativeGasCost(CoordinatorEntity, OctopusEnergyGasSensor, RestoreSensor): """Sensor for displaying the current days accumulative gas cost.""" def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, standing_charge_coordinator, tariff_code, meter, point, calorific_value): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyGasSensor.__init__(self, hass, meter, point) self._hass = hass @@ -89,9 +90,9 @@ def last_reset(self): def state(self): """Retrieve the currently calculated state""" consumption_data = self.coordinator.data if self.coordinator is not None and self.coordinator.data is not None else None - rate_data = self._rates_coordinator.data[self._mprn] if self._rates_coordinator is not None and self._rates_coordinator.data is not None and self._mprn in self._rates_coordinator.data else None - standing_charge = self._standing_charge_coordinator.data[self._mprn]["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None and self._mprn in self._standing_charge_coordinator.data and "value_inc_vat" in self._standing_charge_coordinator.data[self._mprn] else None - + rate_data = self._rates_coordinator.data.rates if self._rates_coordinator is not None and self._rates_coordinator.data is not None else None + standing_charge = self._standing_charge_coordinator.data.standing_charge["value_inc_vat"] if self._standing_charge_coordinator is not None and self._standing_charge_coordinator.data is not None else None + consumption_and_cost = calculate_gas_consumption_and_cost( consumption_data, rate_data, diff --git a/custom_components/octopus_energy/gas/current_consumption.py b/custom_components/octopus_energy/gas/current_consumption.py index 9c71344d..b2c4b9e9 100644 --- a/custom_components/octopus_energy/gas/current_consumption.py +++ b/custom_components/octopus_energy/gas/current_consumption.py @@ -7,8 +7,9 @@ CoordinatorEntity ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass + RestoreSensor, + SensorDeviceClass, + SensorStateClass ) from homeassistant.const import ( ENERGY_KILO_WATT_HOUR @@ -20,12 +21,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyCurrentGasConsumption(CoordinatorEntity, OctopusEnergyGasSensor): +class OctopusEnergyCurrentGasConsumption(CoordinatorEntity, OctopusEnergyGasSensor, RestoreSensor): """Sensor for displaying the current gas consumption.""" def __init__(self, hass: HomeAssistant, coordinator, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyGasSensor.__init__(self, hass, meter, point) self._state = None diff --git a/custom_components/octopus_energy/gas/current_rate.py b/custom_components/octopus_energy/gas/current_rate.py index 462bf94c..507a3194 100644 --- a/custom_components/octopus_energy/gas/current_rate.py +++ b/custom_components/octopus_energy/gas/current_rate.py @@ -8,8 +8,9 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass + RestoreSensor, + SensorDeviceClass, + SensorStateClass ) from .base import (OctopusEnergyGasSensor) @@ -17,12 +18,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyGasCurrentRate(CoordinatorEntity, OctopusEnergyGasSensor): +class OctopusEnergyGasCurrentRate(CoordinatorEntity, OctopusEnergyGasSensor, RestoreSensor): """Sensor for displaying the current rate.""" def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point, gas_price_cap): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyGasSensor.__init__(self, hass, meter, point) self._tariff_code = tariff_code @@ -82,10 +83,11 @@ def extra_state_attributes(self): def state(self): """Retrieve the current rate for the sensor.""" current = now() + rates = self.coordinator.data.rates if self.coordinator is not None and self.coordinator.data is not None else None if (self._last_updated is None or self._last_updated < (current - timedelta(minutes=30)) or (current.minute % 30) == 0): _LOGGER.debug(f"Updating OctopusEnergyGasCurrentRate for '{self._mprn}/{self._serial_number}'") - rate_information = get_current_rate_information(self.coordinator.data[self._mprn] if self.coordinator is not None and self._mprn in self.coordinator.data else None, current) + rate_information = get_current_rate_information(rates, current) if rate_information is not None: self._attributes = { diff --git a/custom_components/octopus_energy/gas/next_rate.py b/custom_components/octopus_energy/gas/next_rate.py index 6a2d21be..7cfca6cb 100644 --- a/custom_components/octopus_energy/gas/next_rate.py +++ b/custom_components/octopus_energy/gas/next_rate.py @@ -8,8 +8,9 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass + RestoreSensor, + SensorDeviceClass, + SensorStateClass ) from .base import (OctopusEnergyGasSensor) @@ -17,12 +18,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyGasNextRate(CoordinatorEntity, OctopusEnergyGasSensor): +class OctopusEnergyGasNextRate(CoordinatorEntity, OctopusEnergyGasSensor, RestoreSensor): """Sensor for displaying the next rate.""" def __init__(self, hass: HomeAssistant, coordinator, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyGasSensor.__init__(self, hass, meter, point) self._state = None @@ -77,10 +78,11 @@ def extra_state_attributes(self): def state(self): """Retrieve the next rate for the sensor.""" current = now() + rates = self.coordinator.data.rates if self.coordinator is not None and self.coordinator.data is not None else None if (self._last_updated is None or self._last_updated < (current - timedelta(minutes=30)) or (current.minute % 30) == 0): _LOGGER.debug(f"Updating OctopusEnergyGasNextRate for '{self._mprn}/{self._serial_number}'") - rate_information = get_next_rate_information(self.coordinator.data[self._mprn] if self.coordinator is not None and self._mprn in self.coordinator.data else None, current) + rate_information = get_next_rate_information(rates, current) if rate_information is not None: self._attributes = { diff --git a/custom_components/octopus_energy/gas/previous_accumulative_consumption.py b/custom_components/octopus_energy/gas/previous_accumulative_consumption.py index d33828a2..5b8f0eb1 100644 --- a/custom_components/octopus_energy/gas/previous_accumulative_consumption.py +++ b/custom_components/octopus_energy/gas/previous_accumulative_consumption.py @@ -1,14 +1,14 @@ import logging from datetime import datetime -from ..statistics.consumption import async_import_external_statistics_from_consumption -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass + RestoreSensor, + SensorDeviceClass, + SensorStateClass ) from homeassistant.const import ( VOLUME_CUBIC_METERS @@ -20,17 +20,22 @@ from .base import (OctopusEnergyGasSensor) +from ..api_client import OctopusEnergyApiClient +from ..statistics.consumption import async_import_external_statistics_from_consumption, get_gas_consumption_statistic_unique_id +from ..statistics.refresh import async_refresh_previous_gas_consumption_data + _LOGGER = logging.getLogger(__name__) -class OctopusEnergyPreviousAccumulativeGasConsumption(CoordinatorEntity, OctopusEnergyGasSensor): +class OctopusEnergyPreviousAccumulativeGasConsumption(CoordinatorEntity, OctopusEnergyGasSensor, RestoreSensor): """Sensor for displaying the previous days accumulative gas reading.""" - def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point, calorific_value): + def __init__(self, hass: HomeAssistant, client: OctopusEnergyApiClient, coordinator, tariff_code, meter, point, calorific_value): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyGasSensor.__init__(self, hass, meter, point) self._hass = hass + self._client = client self._tariff_code = tariff_code self._native_consumption_units = meter["consumption_units"] self._state = None @@ -111,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): @@ -121,7 +124,7 @@ async def async_update(self): await async_import_external_statistics_from_consumption( self._hass, - f"gas_{self._serial_number}_{self._mprn}_previous_accumulative_consumption", + get_gas_consumption_statistic_unique_id(self._serial_number, self._mprn), self.name, consumption_and_cost["charges"], rate_data, @@ -164,4 +167,19 @@ async def async_added_to_hass(self): if x == "last_reset": self._last_reset = datetime.strptime(state.attributes[x], "%Y-%m-%dT%H:%M:%S%z") - _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeGasConsumption state: {self._state}') \ No newline at end of file + _LOGGER.debug(f'Restored OctopusEnergyPreviousAccumulativeGasConsumption state: {self._state}') + + @callback + async def async_refresh_previous_consumption_data(self, start_date): + """Update sensors config""" + + await async_refresh_previous_gas_consumption_data( + self._hass, + self._client, + start_date, + self._mprn, + self._serial_number, + self._tariff_code, + self._native_consumption_units, + self._calorific_value, + ) \ No newline at end of file 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 6c6d037f..c546f2ce 100644 --- a/custom_components/octopus_energy/gas/previous_accumulative_consumption_kwh.py +++ b/custom_components/octopus_energy/gas/previous_accumulative_consumption_kwh.py @@ -7,8 +7,9 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass + RestoreSensor, + SensorDeviceClass, + SensorStateClass ) from homeassistant.const import ( ENERGY_KILO_WATT_HOUR @@ -22,12 +23,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyPreviousAccumulativeGasConsumptionKwh(CoordinatorEntity, OctopusEnergyGasSensor): +class OctopusEnergyPreviousAccumulativeGasConsumptionKwh(CoordinatorEntity, OctopusEnergyGasSensor, RestoreSensor): """Sensor for displaying the previous days accumulative gas consumption in kwh.""" def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point, calorific_value): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyGasSensor.__init__(self, hass, meter, point) self._hass = hass @@ -111,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 96c74a73..022a6c03 100644 --- a/custom_components/octopus_energy/gas/previous_accumulative_cost.py +++ b/custom_components/octopus_energy/gas/previous_accumulative_cost.py @@ -7,8 +7,9 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass + RestoreSensor, + SensorDeviceClass, + SensorStateClass ) from . import ( calculate_gas_consumption_and_cost, @@ -16,16 +17,16 @@ from .base import (OctopusEnergyGasSensor) -from ..statistics.cost import async_import_external_statistics_from_cost +from ..statistics.cost import async_import_external_statistics_from_cost, get_gas_cost_statistic_unique_id _LOGGER = logging.getLogger(__name__) -class OctopusEnergyPreviousAccumulativeGasCost(CoordinatorEntity, OctopusEnergyGasSensor): +class OctopusEnergyPreviousAccumulativeGasCost(CoordinatorEntity, OctopusEnergyGasSensor, RestoreSensor): """Sensor for displaying the previous days accumulative gas cost.""" def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point, calorific_value): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyGasSensor.__init__(self, hass, meter, point) self._hass = hass @@ -110,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): @@ -120,7 +119,7 @@ async def async_update(self): await async_import_external_statistics_from_cost( self._hass, - f"gas_{self._serial_number}_{self._mprn}_previous_accumulative_cost", + get_gas_cost_statistic_unique_id(self._serial_number, self._mprn), self.name, consumption_and_cost["charges"], rate_data, 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 5c4bf079..d472fc05 100644 --- a/custom_components/octopus_energy/gas/previous_accumulative_cost_override.py +++ b/custom_components/octopus_energy/gas/previous_accumulative_cost_override.py @@ -1,5 +1,6 @@ import logging from datetime import (datetime) +import asyncio from homeassistant.core import HomeAssistant @@ -7,8 +8,9 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass + RestoreSensor, + SensorDeviceClass, + SensorStateClass ) from . import ( @@ -20,16 +22,16 @@ 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__) -class OctopusEnergyPreviousAccumulativeGasCostOverride(CoordinatorEntity, OctopusEnergyGasSensor): +class OctopusEnergyPreviousAccumulativeGasCostOverride(CoordinatorEntity, OctopusEnergyGasSensor, RestoreSensor): """Sensor for displaying the previous days accumulative gas cost for a different tariff.""" def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, tariff_code, meter, point, calorific_value): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyGasSensor.__init__(self, hass, meter, point) self._hass = hass @@ -107,18 +109,21 @@ async def async_update(self): consumption_data = self.coordinator.data["consumption"] if self.coordinator is not None and self.coordinator.data is not None and "consumption" in self.coordinator.data else None tariff_override_key = get_gas_tariff_override_key(self._serial_number, self._mprn) - is_old_data = self._last_reset is None or self._last_reset < consumption_data[-1]["interval_end"] + is_old_data = self._last_reset is None or (consumption_data is not None and self._last_reset < consumption_data[-1]["interval_end"]) 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] period_from = consumption_data[0]["interval_start"] period_to = consumption_data[-1]["interval_end"] - rate_data = await self._client.async_get_gas_rates(tariff_override, period_from, period_to) - standing_charge = await self._client.async_get_gas_standing_charge(tariff_override, period_from, period_to) + + [rate_data, standing_charge] = await asyncio.gather( + self._client.async_get_gas_rates(tariff_override, period_from, period_to), + self._client.async_get_gas_standing_charge(tariff_override, period_from, period_to) + ) consumption_and_cost = calculate_gas_consumption_and_cost( consumption_data, @@ -127,9 +132,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 @@ -157,6 +160,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, "serial_number": self._serial_number, "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/previous_accumulative_cost_override_tariff.py b/custom_components/octopus_energy/gas/previous_accumulative_cost_override_tariff.py index ff5c276f..f20614f4 100644 --- a/custom_components/octopus_energy/gas/previous_accumulative_cost_override_tariff.py +++ b/custom_components/octopus_energy/gas/previous_accumulative_cost_override_tariff.py @@ -16,43 +16,26 @@ from ..api_client import OctopusEnergyApiClient +from .base import (OctopusEnergyGasSensor) + _LOGGER = logging.getLogger(__name__) -class OctopusEnergyPreviousAccumulativeGasCostTariffOverride(TextEntity, RestoreEntity): +class OctopusEnergyPreviousAccumulativeGasCostTariffOverride(OctopusEnergyGasSensor, TextEntity, RestoreEntity): """Sensor for the tariff for the previous days accumulative gas cost looking at a different tariff.""" _attr_pattern = REGEX_TARIFF_PARTS def __init__(self, hass: HomeAssistant, client: OctopusEnergyApiClient, tariff_code, meter, point): """Init sensor.""" - - self._point = point - self._meter = meter - - self._mprn = point["mprn"] - self._serial_number = meter["serial_number"] + OctopusEnergyGasSensor.__init__(self, hass, meter, point) - self._attributes = { - "mprn": self._mprn, - "serial_number": self._serial_number - } - - self.entity_id = generate_entity_id("sensor.{}", self.unique_id, hass=hass) + self.entity_id = generate_entity_id("text.{}", self.unique_id, hass=hass) self._hass = hass self._client = client self._tariff_code = tariff_code self._attr_native_value = tariff_code - - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, f"gas_{self._serial_number}_{self._mprn}")}, - name="Gas Meter", - connections=set(), - manufacturer=self._meter["manufacturer"], - model=self._meter["model"], - sw_version=self._meter["firmware"] - ) @property def entity_registry_enabled_default(self) -> bool: diff --git a/custom_components/octopus_energy/gas/previous_rate.py b/custom_components/octopus_energy/gas/previous_rate.py index 53721a32..6bb02236 100644 --- a/custom_components/octopus_energy/gas/previous_rate.py +++ b/custom_components/octopus_energy/gas/previous_rate.py @@ -8,8 +8,9 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass + RestoreSensor, + SensorDeviceClass, + SensorStateClass ) from .base import (OctopusEnergyGasSensor) @@ -17,12 +18,12 @@ _LOGGER = logging.getLogger(__name__) -class OctopusEnergyGasPreviousRate(CoordinatorEntity, OctopusEnergyGasSensor): +class OctopusEnergyGasPreviousRate(CoordinatorEntity, OctopusEnergyGasSensor, RestoreSensor): """Sensor for displaying the previous rate.""" def __init__(self, hass: HomeAssistant, coordinator, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyGasSensor.__init__(self, hass, meter, point) self._state = None @@ -77,10 +78,11 @@ def extra_state_attributes(self): def state(self): """Retrieve the previous rate for the sensor.""" current = now() + rates = self.coordinator.data.rates if self.coordinator is not None and self.coordinator.data is not None else None if (self._last_updated is None or self._last_updated < (current - timedelta(minutes=30)) or (current.minute % 30) == 0): _LOGGER.debug(f"Updating OctopusEnergyGasPreviousRate for '{self._mprn}/{self._serial_number}'") - rate_information = get_previous_rate_information(self.coordinator.data[self._mprn] if self.coordinator is not None and self._mprn in self.coordinator.data else None, current) + rate_information = get_previous_rate_information(rates, current) if rate_information is not None: self._attributes = { 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..a422b750 --- /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 and "serial_number" in event.data and event.data["serial_number"] == self._serial_number): + 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..8ab2970f --- /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 and "serial_number" in event.data and event.data["serial_number"] == self._serial_number): + 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..5ffc0628 --- /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 and "serial_number" in event.data and event.data["serial_number"] == self._serial_number): + 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..beadb371 --- /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 and "serial_number" in event.data and event.data["serial_number"] == self._serial_number): + 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..adfff25d --- /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 and "serial_number" in event.data and event.data["serial_number"] == self._serial_number): + 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/standing_charge.py b/custom_components/octopus_energy/gas/standing_charge.py index 88e077d5..0843f8e2 100644 --- a/custom_components/octopus_energy/gas/standing_charge.py +++ b/custom_components/octopus_energy/gas/standing_charge.py @@ -6,20 +6,21 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass + RestoreSensor, + SensorDeviceClass, + SensorStateClass ) from .base import (OctopusEnergyGasSensor) _LOGGER = logging.getLogger(__name__) -class OctopusEnergyGasCurrentStandingCharge(CoordinatorEntity, OctopusEnergyGasSensor): +class OctopusEnergyGasCurrentStandingCharge(CoordinatorEntity, OctopusEnergyGasSensor, RestoreSensor): """Sensor for displaying the current standing charge.""" def __init__(self, hass: HomeAssistant, coordinator, tariff_code, meter, point): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyGasSensor.__init__(self, hass, meter, point) self._tariff_code = tariff_code @@ -67,7 +68,7 @@ def state(self): """Retrieve the latest gas standing charge""" _LOGGER.debug('Updating OctopusEnergyGasCurrentStandingCharge') - standard_charge_result = self.coordinator.data[self._mprn] if self.coordinator is not None and self.coordinator.data is not None and self._mprn in self.coordinator.data else None + standard_charge_result = self.coordinator.data.standing_charge if self.coordinator is not None and self.coordinator.data is not None else None if standard_charge_result is not None: self._latest_date = standard_charge_result["valid_from"] diff --git a/custom_components/octopus_energy/intelligent/__init__.py b/custom_components/octopus_energy/intelligent/__init__.py index 4848617b..67cf71af 100644 --- a/custom_components/octopus_energy/intelligent/__init__.py +++ b/custom_components/octopus_energy/intelligent/__init__.py @@ -5,7 +5,7 @@ from homeassistant.helpers import storage -from ..utils import get_tariff_parts +from ..utils import get_active_tariff_code, get_tariff_parts from ..const import DOMAIN @@ -84,6 +84,15 @@ def is_intelligent_tariff(tariff_code: str): return parts is not None and "INTELLI" in parts.product_code +def has_intelligent_tariff(current: datetime, account_info): + if account_info is not None and len(account_info["electricity_meter_points"]) > 0: + for point in account_info["electricity_meter_points"]: + tariff_code = get_active_tariff_code(current, point["agreements"]) + if tariff_code is not None and is_intelligent_tariff(tariff_code): + return True + + return False + def __get_dispatch(rate, dispatches, expected_source: str): for dispatch in dispatches: if (expected_source is None or dispatch["source"] == expected_source) and dispatch["start"] <= rate["valid_from"] and dispatch["end"] >= rate["valid_to"]: diff --git a/custom_components/octopus_energy/intelligent/bump_charge.py b/custom_components/octopus_energy/intelligent/bump_charge.py index 41000109..57f18494 100644 --- a/custom_components/octopus_energy/intelligent/bump_charge.py +++ b/custom_components/octopus_energy/intelligent/bump_charge.py @@ -21,7 +21,7 @@ class OctopusEnergyIntelligentBumpCharge(CoordinatorEntity, SwitchEntity, Octopu def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, device, account_id: str): """Init sensor.""" # Pass coordinator to base class - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyIntelligentSensor.__init__(self, device) self._state = False diff --git a/custom_components/octopus_energy/intelligent/charge_limit.py b/custom_components/octopus_energy/intelligent/charge_limit.py index 18c90410..1475f934 100644 --- a/custom_components/octopus_energy/intelligent/charge_limit.py +++ b/custom_components/octopus_energy/intelligent/charge_limit.py @@ -22,7 +22,7 @@ class OctopusEnergyIntelligentChargeLimit(CoordinatorEntity, RestoreNumber, Octo def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, device, account_id: str): """Init sensor.""" # Pass coordinator to base class - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyIntelligentSensor.__init__(self, device) self._state = None diff --git a/custom_components/octopus_energy/intelligent/dispatching.py b/custom_components/octopus_energy/intelligent/dispatching.py index 6e4f2b19..62dba903 100644 --- a/custom_components/octopus_energy/intelligent/dispatching.py +++ b/custom_components/octopus_energy/intelligent/dispatching.py @@ -29,7 +29,7 @@ class OctopusEnergyIntelligentDispatching(CoordinatorEntity, BinarySensorEntity, def __init__(self, hass: HomeAssistant, coordinator, rates_coordinator, mpan, device): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyIntelligentSensor.__init__(self, device) self._rates_coordinator = rates_coordinator @@ -69,6 +69,7 @@ def extra_state_attributes(self): def is_on(self): """Determine if OE is currently dispatching energy.""" dispatches = self.coordinator.data if self.coordinator is not None else None + rates = self.coordinator.data.rates if self.coordinator is not None and self.coordinator.data is not None else None if (dispatches is not None): self._attributes["planned_dispatches"] = self.coordinator.data["planned"] self._attributes["completed_dispatches"] = self.coordinator.data["completed"] @@ -80,7 +81,7 @@ def is_on(self): self._attributes["completed_dispatches"] = [] current_date = now() - self._state = is_in_planned_dispatch(current_date, self._attributes["planned_dispatches"]) or is_off_peak(current_date, self._rates_coordinator.data[self._mpan] if self._rates_coordinator is not None and self._mpan in self._rates_coordinator.data else None) + self._state = is_in_planned_dispatch(current_date, self._attributes["planned_dispatches"]) or is_off_peak(current_date, rates) return self._state diff --git a/custom_components/octopus_energy/intelligent/ready_time.py b/custom_components/octopus_energy/intelligent/ready_time.py index 99983b12..c68a28eb 100644 --- a/custom_components/octopus_energy/intelligent/ready_time.py +++ b/custom_components/octopus_energy/intelligent/ready_time.py @@ -21,7 +21,7 @@ class OctopusEnergyIntelligentReadyTime(CoordinatorEntity, TimeEntity, OctopusEn def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, device, account_id: str): """Init sensor.""" # Pass coordinator to base class - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyIntelligentSensor.__init__(self, device) self._state = time() diff --git a/custom_components/octopus_energy/intelligent/smart_charge.py b/custom_components/octopus_energy/intelligent/smart_charge.py index 28b87d04..cd7e9be0 100644 --- a/custom_components/octopus_energy/intelligent/smart_charge.py +++ b/custom_components/octopus_energy/intelligent/smart_charge.py @@ -20,7 +20,7 @@ class OctopusEnergyIntelligentSmartCharge(CoordinatorEntity, SwitchEntity, Octop def __init__(self, hass: HomeAssistant, coordinator, client: OctopusEnergyApiClient, device, account_id: str): """Init sensor.""" # Pass coordinator to base class - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) OctopusEnergyIntelligentSensor.__init__(self, device) self._state = False diff --git a/custom_components/octopus_energy/manifest.json b/custom_components/octopus_energy/manifest.json index 35ffd517..799f26a9 100644 --- a/custom_components/octopus_energy/manifest.json +++ b/custom_components/octopus_energy/manifest.json @@ -9,7 +9,7 @@ "repairs", "recorder" ], - "documentation": "https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/", + "documentation": "https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/tree/develop/_docs", "homekit": {}, "iot_class": "cloud_polling", "issue_tracker": "https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/issues", diff --git a/custom_components/octopus_energy/saving_sessions/points.py b/custom_components/octopus_energy/saving_sessions/points.py index 09b81d6b..43c625c3 100644 --- a/custom_components/octopus_energy/saving_sessions/points.py +++ b/custom_components/octopus_energy/saving_sessions/points.py @@ -7,20 +7,19 @@ CoordinatorEntity, ) from homeassistant.components.sensor import ( - SensorEntity, - SensorStateClass + RestoreSensor, + SensorStateClass ) -from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) -class OctopusEnergySavingSessionPoints(CoordinatorEntity, SensorEntity, RestoreEntity): +class OctopusEnergySavingSessionPoints(CoordinatorEntity, RestoreSensor): """Sensor for determining saving session points""" def __init__(self, hass: HomeAssistant, coordinator): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) self._state = None self._attributes = {} diff --git a/custom_components/octopus_energy/saving_sessions/saving_sessions.py b/custom_components/octopus_energy/saving_sessions/saving_sessions.py index c73dc819..ba7fd387 100644 --- a/custom_components/octopus_energy/saving_sessions/saving_sessions.py +++ b/custom_components/octopus_energy/saving_sessions/saving_sessions.py @@ -25,7 +25,7 @@ class OctopusEnergySavingSessions(CoordinatorEntity, BinarySensorEntity, Restore def __init__(self, hass: HomeAssistant, coordinator): """Init sensor.""" - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) self._state = None self._events = [] diff --git a/custom_components/octopus_energy/sensor.py b/custom_components/octopus_energy/sensor.py index fb695bbf..190c2ad8 100644 --- a/custom_components/octopus_energy/sensor.py +++ b/custom_components/octopus_energy/sensor.py @@ -1,6 +1,9 @@ +import voluptuous as vol import logging + from homeassistant.util.dt import (utcnow) from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_platform from .electricity.current_consumption import OctopusEnergyCurrentElectricityConsumption from .electricity.current_accumulative_consumption import OctopusEnergyCurrentAccumulativeElectricityConsumption @@ -34,7 +37,7 @@ from .gas.previous_accumulative_cost_override import OctopusEnergyPreviousAccumulativeGasCostOverride from .coordinators.current_consumption import async_create_current_consumption_coordinator -from .coordinators.gas_rates import async_create_gas_rate_coordinator +from .coordinators.gas_rates import async_setup_gas_rates_coordinator from .coordinators.previous_consumption_and_rates import async_create_previous_consumption_and_rates_coordinator from .coordinators.electricity_standing_charges import async_setup_electricity_standing_charges_coordinator from .coordinators.gas_standing_charges import async_setup_gas_standing_charges_coordinator @@ -59,7 +62,7 @@ CONFIG_MAIN_GAS_PRICE_CAP, CONFIG_MAIN_ACCOUNT_ID, - DATA_ELECTRICITY_RATES_COORDINATOR, + DATA_ELECTRICITY_RATES_COORDINATOR_KEY, DATA_SAVING_SESSIONS_COORDINATOR, DATA_CLIENT, DATA_ACCOUNT @@ -73,6 +76,20 @@ async def async_setup_entry(hass, entry, async_add_entities): if CONFIG_MAIN_API_KEY in entry.data: await async_setup_default_sensors(hass, entry, async_add_entities) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + "refresh_previous_consumption_data", + vol.All( + vol.Schema( + { + vol.Optional("start_time"): str, + }, + extra=vol.ALLOW_EXTRA, + ), + ), + "async_refresh_previous_consumption_data", + ) + async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_entities): config = dict(entry.data) @@ -91,11 +108,6 @@ async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_enti now = utcnow() if len(account_info["electricity_meter_points"]) > 0: - electricity_rate_coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR] - await electricity_rate_coordinator.async_config_entry_first_refresh() - - electricity_standing_charges_coordinator = await async_setup_electricity_standing_charges_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) - electricity_price_cap = None if CONFIG_MAIN_ELECTRICITY_PRICE_CAP in config: electricity_price_cap = config[CONFIG_MAIN_ELECTRICITY_PRICE_CAP] @@ -109,7 +121,14 @@ async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_enti electricity_tariff_code = get_active_tariff_code(now, point["agreements"]) if electricity_tariff_code is not None: for meter in point["meters"]: - _LOGGER.info(f'Adding electricity meter; mpan: {point["mpan"]}; serial number: {meter["serial_number"]}') + mpan = point["mpan"] + serial_number = meter["serial_number"] + + _LOGGER.info(f'Adding electricity meter; mpan: {mpan}; serial number: {serial_number}') + + electricity_rate_coordinator = hass.data[DOMAIN][DATA_ELECTRICITY_RATES_COORDINATOR_KEY.format(mpan, serial_number)] + electricity_standing_charges_coordinator = await async_setup_electricity_standing_charges_coordinator(hass, mpan, serial_number) + entities.append(OctopusEnergyElectricityCurrentRate(hass, electricity_rate_coordinator, meter, point, electricity_tariff_code, electricity_price_cap)) entities.append(OctopusEnergyElectricityPreviousRate(hass, electricity_rate_coordinator, meter, point)) entities.append(OctopusEnergyElectricityNextRate(hass, electricity_rate_coordinator, meter, point)) @@ -118,14 +137,14 @@ async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_enti previous_consumption_coordinator = await async_create_previous_consumption_and_rates_coordinator( hass, client, - point["mpan"], - meter["serial_number"], + mpan, + serial_number, True, electricity_tariff_code, meter["is_smart_meter"], previous_electricity_consumption_days_offset ) - entities.append(OctopusEnergyPreviousAccumulativeElectricityConsumption(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) + entities.append(OctopusEnergyPreviousAccumulativeElectricityConsumption(hass, client, previous_consumption_coordinator, electricity_tariff_code, meter, point)) entities.append(OctopusEnergyPreviousAccumulativeElectricityConsumptionPeak(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) entities.append(OctopusEnergyPreviousAccumulativeElectricityConsumptionOffPeak(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) entities.append(OctopusEnergyPreviousAccumulativeElectricityCost(hass, previous_consumption_coordinator, electricity_tariff_code, meter, point)) @@ -164,9 +183,6 @@ async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_enti if CONFIG_MAIN_GAS_PRICE_CAP in config: gas_price_cap = config[CONFIG_MAIN_GAS_PRICE_CAP] - gas_rate_coordinator = await async_create_gas_rate_coordinator(hass, client) - gas_standing_charges_coordinator = await async_setup_gas_standing_charges_coordinator(hass, config[CONFIG_MAIN_ACCOUNT_ID]) - previous_gas_consumption_days_offset = CONFIG_DEFAULT_PREVIOUS_CONSUMPTION_OFFSET_IN_DAYS if CONFIG_MAIN_PREVIOUS_GAS_CONSUMPTION_DAYS_OFFSET in config: previous_gas_consumption_days_offset = config[CONFIG_MAIN_PREVIOUS_GAS_CONSUMPTION_DAYS_OFFSET] @@ -176,7 +192,14 @@ async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_enti gas_tariff_code = get_active_tariff_code(now, point["agreements"]) if gas_tariff_code is not None: for meter in point["meters"]: - _LOGGER.info(f'Adding gas meter; mprn: {point["mprn"]}; serial number: {meter["serial_number"]}') + mprn = point["mprn"] + serial_number = meter["serial_number"] + + _LOGGER.info(f'Adding gas meter; mprn: {mprn}; serial number: {serial_number}') + + gas_rate_coordinator = await async_setup_gas_rates_coordinator(hass, client, mprn, serial_number) + gas_standing_charges_coordinator = await async_setup_gas_standing_charges_coordinator(hass, mprn, serial_number) + entities.append(OctopusEnergyGasCurrentRate(hass, gas_rate_coordinator, gas_tariff_code, meter, point, gas_price_cap)) entities.append(OctopusEnergyGasPreviousRate(hass, gas_rate_coordinator, meter, point)) entities.append(OctopusEnergyGasNextRate(hass, gas_rate_coordinator, meter, point)) @@ -185,14 +208,14 @@ async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_enti previous_consumption_coordinator = await async_create_previous_consumption_and_rates_coordinator( hass, client, - point["mprn"], - meter["serial_number"], + mprn, + serial_number, False, gas_tariff_code, None, previous_gas_consumption_days_offset ) - entities.append(OctopusEnergyPreviousAccumulativeGasConsumption(hass, previous_consumption_coordinator, gas_tariff_code, meter, point, calorific_value)) + entities.append(OctopusEnergyPreviousAccumulativeGasConsumption(hass, client, previous_consumption_coordinator, gas_tariff_code, meter, point, calorific_value)) entities.append(OctopusEnergyPreviousAccumulativeGasConsumptionKwh(hass, previous_consumption_coordinator, gas_tariff_code, meter, point, calorific_value)) entities.append(OctopusEnergyPreviousAccumulativeGasCost(hass, previous_consumption_coordinator, gas_tariff_code, meter, point, calorific_value)) entities.append(OctopusEnergyPreviousAccumulativeGasCostOverride(hass, previous_consumption_coordinator, client, gas_tariff_code, meter, point, calorific_value)) diff --git a/custom_components/octopus_energy/services.yaml b/custom_components/octopus_energy/services.yaml index dcb1e817..55a90dc6 100644 --- a/custom_components/octopus_energy/services.yaml +++ b/custom_components/octopus_energy/services.yaml @@ -30,7 +30,20 @@ update_target_config: The optional offset to apply to the target rate when it starts selector: text: - purge_invalid_external_statistic_ids: name: Purge invalid external statistics - description: Removes external statistics for all meters that don't have an active tariff \ No newline at end of file + description: Removes external statistics for all meters that don't have an active tariff +refresh_previous_consumption_data: + name: Refresh previous consumption data + description: Refreshes the previous consumption data for a given entity from a given date. + target: + entity: + integration: octopus_energy + domain: sensor + fields: + start_date: + name: Date + description: The date the data should be loaded from. + required: true + selector: + date: \ No newline at end of file diff --git a/custom_components/octopus_energy/statistics/consumption.py b/custom_components/octopus_energy/statistics/consumption.py index f9089e10..e2689aaa 100644 --- a/custom_components/octopus_energy/statistics/consumption.py +++ b/custom_components/octopus_energy/statistics/consumption.py @@ -11,6 +11,18 @@ _LOGGER = logging.getLogger(__name__) +def get_electricity_consumption_statistic_unique_id(serial_number: str, mpan: str, is_export: bool): + return f"electricity_{serial_number}_{mpan}{'_export' if is_export == True else ''}_previous_accumulative_consumption" + +def get_electricity_consumption_statistic_name(serial_number: str, mpan: str, is_export: bool): + return f"Electricity {serial_number} {mpan}{' Export' if is_export == True else ''} Previous Accumulative Consumption" + +def get_gas_consumption_statistic_unique_id(serial_number: str, mpan: str): + return f"gas_{serial_number}_{mpan}_previous_accumulative_consumption" + +def get_gas_consumption_statistic_name(serial_number: str, mpan: str): + return f"Gas {serial_number} {mpan} Previous Accumulative Consumption" + async def async_import_external_statistics_from_consumption( hass: HomeAssistant, unique_id: str, diff --git a/custom_components/octopus_energy/statistics/cost.py b/custom_components/octopus_energy/statistics/cost.py index 61913162..6cf1e27c 100644 --- a/custom_components/octopus_energy/statistics/cost.py +++ b/custom_components/octopus_energy/statistics/cost.py @@ -11,6 +11,18 @@ _LOGGER = logging.getLogger(__name__) +def get_electricity_cost_statistic_unique_id(serial_number: str, mpan: str, is_export: bool): + return f"electricity_{serial_number}_{mpan}{'_export' if is_export == True else ''}_previous_accumulative_cost" + +def get_electricity_cost_statistic_name(serial_number: str, mpan: str, is_export: bool): + return f"Electricity {serial_number} {mpan}{' Export' if is_export == True else ''} Previous Accumulative Cost" + +def get_gas_cost_statistic_unique_id(serial_number: str, mpan: str): + return f"gas_{serial_number}_{mpan}_previous_accumulative_cost" + +def get_gas_cost_statistic_name(serial_number: str, mpan: str): + return f"Gas {serial_number} {mpan} Previous Accumulative Cost" + async def async_import_external_statistics_from_cost( hass: HomeAssistant, unique_id: str, diff --git a/custom_components/octopus_energy/statistics/refresh.py b/custom_components/octopus_energy/statistics/refresh.py new file mode 100644 index 00000000..1029661b --- /dev/null +++ b/custom_components/octopus_energy/statistics/refresh.py @@ -0,0 +1,155 @@ +from datetime import timedelta +import re +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components import persistent_notification +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + VOLUME_CUBIC_METERS +) + +from homeassistant.util.dt import (now, parse_datetime) + +from ..api_client import OctopusEnergyApiClient +from ..const import REGEX_DATE +from .consumption import async_import_external_statistics_from_consumption, get_electricity_consumption_statistic_name, get_electricity_consumption_statistic_unique_id, get_gas_consumption_statistic_name, get_gas_consumption_statistic_unique_id +from .cost import async_import_external_statistics_from_cost, get_electricity_cost_statistic_name, get_electricity_cost_statistic_unique_id, get_gas_cost_statistic_name, get_gas_cost_statistic_unique_id +from ..electricity import calculate_electricity_consumption_and_cost +from ..gas import calculate_gas_consumption_and_cost + +async def async_refresh_previous_electricity_consumption_data( + hass: HomeAssistant, + client: OctopusEnergyApiClient, + start_date: str, + mpan: str, + serial_number: str, + tariff_code: str, + is_smart_meter: bool, + is_export: bool +): + # Inputs from automations can include quotes, so remove these + trimmed_date = start_date.strip('\"') + matches = re.search(REGEX_DATE, trimmed_date) + if matches is None: + raise vol.Invalid(f"Date '{trimmed_date}' must match format of YYYY-MM-DD.") + + persistent_notification.async_create( + hass, + title="Consumption data refreshing started", + message=f"Consumption data from {start_date} for electricity meter {serial_number}/{mpan} has started" + ) + + period_from = parse_datetime(f'{trimmed_date}T00:00:00Z') + while period_from < now(): + period_to = period_from + timedelta(days=2) + + consumption_data = await client.async_get_electricity_consumption(mpan, serial_number, period_from, period_to) + rates = await client.async_get_electricity_rates(tariff_code, is_smart_meter, period_from, period_to) + + consumption_and_cost = calculate_electricity_consumption_and_cost( + consumption_data, + rates, + 0, + None, + tariff_code + ) + + if consumption_and_cost is not None: + await async_import_external_statistics_from_consumption( + hass, + get_electricity_consumption_statistic_unique_id(serial_number, mpan, is_export), + get_electricity_consumption_statistic_name(serial_number, mpan, is_export), + consumption_and_cost["charges"], + rates, + ENERGY_KILO_WATT_HOUR, + "consumption" + ) + + await async_import_external_statistics_from_cost( + hass, + get_electricity_cost_statistic_unique_id(serial_number, mpan, is_export), + get_electricity_cost_statistic_name(serial_number, mpan, is_export), + consumption_and_cost["charges"], + rates, + "GBP", + "consumption" + ) + + period_from = period_to + + persistent_notification.async_create( + hass, + title="Consumption data refreshed", + message=f"Consumption data from {start_date} for electricity meter {serial_number}/{mpan} has finished" + ) + +async def async_refresh_previous_gas_consumption_data( + hass: HomeAssistant, + client: OctopusEnergyApiClient, + start_date: str, + mprn: str, + serial_number: str, + tariff_code: str, + consumption_units: str, + calorific_value: float +): + # Inputs from automations can include quotes, so remove these + trimmed_date = start_date.strip('\"') + matches = re.search(REGEX_DATE, trimmed_date) + if matches is None: + raise vol.Invalid(f"Date '{trimmed_date}' must match format of YYYY-MM-DD.") + + persistent_notification.async_create( + hass, + title="Consumption data refreshing started", + message=f"Consumption data from {start_date} for gas meter {serial_number}/{mprn} has started" + ) + + period_from = parse_datetime(f'{trimmed_date}T00:00:00Z') + while period_from < now(): + period_to = period_from + timedelta(days=2) + + consumption_data = await client.async_get_gas_consumption(mprn, serial_number, period_from, period_to) + rates = await client.async_get_gas_rates(tariff_code, period_from, period_to) + + consumption_and_cost = calculate_gas_consumption_and_cost( + consumption_data, + rates, + 0, + None, + tariff_code, + consumption_units, + calorific_value + ) + + if consumption_and_cost is not None: + await async_import_external_statistics_from_consumption( + hass, + get_gas_consumption_statistic_unique_id(serial_number, mprn), + get_gas_consumption_statistic_name(serial_number, mprn), + consumption_and_cost["charges"], + rates, + VOLUME_CUBIC_METERS, + "consumption_m3", + False + ) + + await async_import_external_statistics_from_cost( + hass, + get_gas_cost_statistic_unique_id(serial_number, mprn), + get_gas_cost_statistic_name(serial_number, mprn), + consumption_and_cost["charges"], + rates, + "GBP", + "consumption_kwh", + False + ) + + period_from = period_to + + persistent_notification.async_create( + hass, + title="Consumption data refreshed", + message=f"Consumption data from {start_date} for gas meter {serial_number}/{mprn} has finished" + ) \ No newline at end of file diff --git a/custom_components/octopus_energy/target_rates/target_rate.py b/custom_components/octopus_energy/target_rates/target_rate.py index e355f774..5ff79bd0 100644 --- a/custom_components/octopus_energy/target_rates/target_rate.py +++ b/custom_components/octopus_energy/target_rates/target_rate.py @@ -48,7 +48,7 @@ class OctopusEnergyTargetRate(CoordinatorEntity, BinarySensorEntity, RestoreEnti def __init__(self, hass: HomeAssistant, coordinator, config, is_export): """Init sensor.""" # Pass coordinator to base class - super().__init__(coordinator) + CoordinatorEntity.__init__(self, coordinator) self._state = None self._config = config @@ -116,15 +116,7 @@ def is_on(self): if all_rates_in_past: if self.coordinator is not None and self.coordinator.data is not None: - all_rates = self.coordinator.data - - # Retrieve our rates. For backwards compatibility, if CONFIG_TARGET_MPAN is not set, then pick the first set - if CONFIG_TARGET_MPAN not in self._config: - _LOGGER.debug(f"'CONFIG_TARGET_MPAN' not set.'{len(all_rates)}' rates available. Retrieving the first rate.") - all_rates = next(iter(all_rates.values())) - else: - _LOGGER.debug(f"Retrieving rates for '{self._config[CONFIG_TARGET_MPAN]}'") - all_rates = all_rates.get(self._config[CONFIG_TARGET_MPAN]) + all_rates = self.coordinator.data.rates else: _LOGGER.debug(f"Rate data missing. Setting to empty array") all_rates = [] diff --git a/hacs.json b/hacs.json index f40e1a23..4ce53188 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { "name": "Octopus Energy", "render_readme": true, - "homeassistant": "2023.6.0" + "homeassistant": "2023.8.0" } \ No newline at end of file diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 9acc71cc..43af83c7 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -83,17 +83,4 @@ def create_rate_data(period_from, period_to, expected_rates: list): if (rate_index > (len(expected_rates) - 1)): rate_index = 0 - return rates - -async def async_get_tracker_tariff(api_key, tariff_code, target_date): - async with aiohttp.ClientSession() as client: - auth = aiohttp.BasicAuth(api_key, '') - url = f'https://octopus.energy/api/v1/tracker/{tariff_code}/daily/past/1/0' - async with client.get(url, auth=auth) as response: - text = await response.text() - data = json.loads(text) - for period in data["periods"]: - valid_from = parse_datetime(f'{period["date"]}T00:00:00Z') - valid_to = parse_datetime(f'{period["date"]}T00:00:00Z') + timedelta(days=1) - if (valid_from <= target_date and valid_to >= target_date): - return period \ No newline at end of file + return rates \ No newline at end of file diff --git a/tests/integration/api_client/test_get_electricity_rates.py b/tests/integration/api_client/test_get_electricity_rates.py index 535f878b..8af3a659 100644 --- a/tests/integration/api_client/test_get_electricity_rates.py +++ b/tests/integration/api_client/test_get_electricity_rates.py @@ -3,7 +3,7 @@ from homeassistant.util.dt import (now) -from integration import (get_test_context, async_get_tracker_tariff) +from integration import (get_test_context) from custom_components.octopus_energy.api_client import OctopusEnergyApiClient default_period_from = datetime.strptime("2022-12-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -160,43 +160,6 @@ async def test_when_get_electricity_rates_is_called_with_duel_rate_tariff_smart_ else: assert item["value_inc_vat"] != cheapest_rate -# @pytest.mark.asyncio -# @pytest.mark.parametrize("tariff,price_cap",[("E-1R-SILVER-FLEX-22-11-25-C", None), ("E-1R-SILVER-FLEX-22-11-25-C", 10)]) -# async def test_when_get_electricity_rates_is_called_with_tracker_tariff_then_rates_are_returned(tariff, price_cap): -# # Arrange -# context = get_test_context() -# period_from = now().replace(hour=0, minute=0, second=0, microsecond=0) -# period_to = (now() + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) -# client = OctopusEnergyApiClient(context["api_key"], price_cap) - -# expected_tracker = await async_get_tracker_tariff(context["api_key"], tariff, now()) -# assert expected_tracker is not None - -# # Act -# data = await client.async_get_electricity_rates(tariff, False, period_from, period_to) - -# # Assert -# assert data is not None -# assert len(data) == 48 - -# # Make sure our data is returned in 30 minute increments -# expected_valid_from = period_from -# for item in data: -# expected_valid_to = expected_valid_from + timedelta(minutes=30) - -# assert "valid_from" in item -# assert item["valid_from"] == expected_valid_from -# assert "valid_to" in item -# assert item["valid_to"] == expected_valid_to - -# assert "value_inc_vat" in item -# if (price_cap is not None and expected_tracker["unit_rate"] > price_cap): -# assert item["value_inc_vat"] == price_cap -# else: -# assert item["value_inc_vat"] == expected_tracker["unit_rate"] - -# expected_valid_from = expected_valid_to - @pytest.mark.asyncio @pytest.mark.parametrize("tariff",[("E-2R-NOT-A-TARIFF-A"), ("E-1R-NOT-A-TARIFF-A"), ("NOT-A-TARIFF")]) async def test_when_get_electricity_rates_is_called_for_non_existent_tariff_then_none_is_returned(tariff): diff --git a/tests/integration/api_client/test_get_electricity_standing_charge.py b/tests/integration/api_client/test_get_electricity_standing_charge.py index 9b71a0c6..c89b2e18 100644 --- a/tests/integration/api_client/test_get_electricity_standing_charge.py +++ b/tests/integration/api_client/test_get_electricity_standing_charge.py @@ -3,7 +3,7 @@ from homeassistant.util.dt import (now) -from integration import (get_test_context, async_get_tracker_tariff) +from integration import (get_test_context) from custom_components.octopus_energy.api_client import OctopusEnergyApiClient period_from = datetime.strptime("2022-12-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -40,16 +40,13 @@ async def test_when_get_electricity_standing_charge_is_called_with_tracker_tarif period_to = (now() + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) client = OctopusEnergyApiClient(context["api_key"]) - expected_tracker = await async_get_tracker_tariff(context["api_key"], tariff, now()) - assert expected_tracker is not None - # Act result = await client.async_get_electricity_standing_charge(tariff, period_from, period_to) # Assert assert result is not None assert "value_inc_vat" in result - assert result["value_inc_vat"] == expected_tracker["standing_charge"] + assert result["value_inc_vat"] is not None @pytest.mark.asyncio @pytest.mark.parametrize("tariff",[("E-1R-NOT-A-TARIFF-A"), ("NOT-A-TARIFF")]) diff --git a/tests/integration/api_client/test_get_gas_rates.py b/tests/integration/api_client/test_get_gas_rates.py index 4fbbd9b1..a1696067 100644 --- a/tests/integration/api_client/test_get_gas_rates.py +++ b/tests/integration/api_client/test_get_gas_rates.py @@ -3,7 +3,7 @@ from homeassistant.util.dt import (now) -from integration import (get_test_context, async_get_tracker_tariff) +from integration import (get_test_context) from custom_components.octopus_energy.api_client import OctopusEnergyApiClient @pytest.mark.asyncio @@ -42,46 +42,6 @@ async def test_when_get_gas_rates_is_called_for_existent_tariff_then_rates_are_r expected_valid_from = expected_valid_to -# @pytest.mark.asyncio -# @pytest.mark.parametrize("tariff,price_cap",[ -# ("G-1R-SILVER-FLEX-22-11-25-C", None), -# ("G-1R-SILVER-FLEX-22-11-25-C", 2), -# ]) -# async def test_when_get_gas_rates_is_called_with_tracker_tariff_then_rates_are_returned(tariff, price_cap): -# # Arrange -# context = get_test_context() -# period_from = now().replace(hour=0, minute=0, second=0, microsecond=0) -# period_to = (now() + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) -# client = OctopusEnergyApiClient(context["api_key"], None, price_cap) - -# expected_tracker = await async_get_tracker_tariff(context["api_key"], tariff, now()) -# assert expected_tracker is not None - -# # Act -# data = await client.async_get_gas_rates(tariff, period_from, period_to) - -# # Assert -# assert data is not None -# assert len(data) == 48 - -# # Make sure our data is returned in 30 minute increments -# expected_valid_from = period_from -# for item in data: -# expected_valid_to = expected_valid_from + timedelta(minutes=30) - -# assert "valid_from" in item -# assert item["valid_from"] == expected_valid_from -# assert "valid_to" in item -# assert item["valid_to"] == expected_valid_to - -# assert "value_inc_vat" in item -# if (price_cap is not None and expected_tracker["unit_rate"] > price_cap): -# assert item["value_inc_vat"] == price_cap -# else: -# assert item["value_inc_vat"] == expected_tracker["unit_rate"] - -# expected_valid_from = expected_valid_to - @pytest.mark.asyncio @pytest.mark.parametrize("tariff",[("G-1R-NOT-A-TARIFF-A"), ("NOT-A-TARIFF")]) async def test_when_get_gas_rates_is_called_for_non_existent_tariff_then_none_is_returned(tariff): diff --git a/tests/integration/api_client/test_get_gas_standing_charge.py b/tests/integration/api_client/test_get_gas_standing_charge.py index 8e2deb06..361291b0 100644 --- a/tests/integration/api_client/test_get_gas_standing_charge.py +++ b/tests/integration/api_client/test_get_gas_standing_charge.py @@ -3,7 +3,7 @@ from homeassistant.util.dt import (now) -from integration import (get_test_context, async_get_tracker_tariff) +from integration import (get_test_context) from custom_components.octopus_energy.api_client import OctopusEnergyApiClient period_from = datetime.strptime("2021-12-01T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") @@ -34,16 +34,13 @@ async def test_when_get_gas_standing_charge_is_called_with_tracker_tariff_then_r period_to = (now() + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) client = OctopusEnergyApiClient(context["api_key"]) - expected_tracker = await async_get_tracker_tariff(context["api_key"], tariff, now()) - assert expected_tracker is not None - # Act result = await client.async_get_gas_standing_charge(tariff, period_from, period_to) # Assert assert result is not None assert "value_inc_vat" in result - assert result["value_inc_vat"] == expected_tracker["standing_charge"] + assert result["value_inc_vat"] is not None @pytest.mark.asyncio @pytest.mark.parametrize("tariff",[("G-1R-NOT-A-TARIFF-A"), ("NOT-A-TARIFF")]) 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..f8a93239 100644 --- a/tests/unit/coordinators/test_async_refresh_electricity_rates_data.py +++ b/tests/unit/coordinators/test_async_refresh_electricity_rates_data.py @@ -5,7 +5,8 @@ from unit import (create_rate_data) 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.coordinators.electricity_rates import ElectricityRatesCoordinatorResult, 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,19 @@ 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 "serial_number" in raised_events[expected_event_name] + assert raised_events[expected_event_name]["serial_number"] == serial_number + 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,27 +66,37 @@ 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]) - } + existing_rates = ElectricityRatesCoordinatorResult(period_from, create_rate_data(period_from, period_to, [2, 4])) dispatches = { "planned": [], "completed": [] } with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") - retrieved_rates = await async_refresh_electricity_rates_data( + retrieved_rates: ElectricityRatesCoordinatorResult = await async_refresh_electricity_rates_data( current, client, account_info, + mpan, + serial_number, + True, + False, 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(): +async def test_when_no_active_rates_then_none_returned(): expected_rates = create_rate_data(period_from, period_to, [1, 2]) rates_returned = False async def async_mocked_get_electricity_rates(*args, **kwargs): @@ -80,24 +104,34 @@ 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]) - } + existing_rates = ElectricityRatesCoordinatorResult(period_from, create_rate_data(period_from, period_to, [2, 4])) dispatches = { "planned": [], "completed": [] } with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") - retrieved_rates = await async_refresh_electricity_rates_data( + retrieved_rates: ElectricityRatesCoordinatorResult = await async_refresh_electricity_rates_data( current, client, account_info, + mpan, + serial_number, + True, + False, existing_rates, - dispatches + dispatches, + fire_event ) - assert retrieved_rates == {} + assert retrieved_rates is None 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,167 +147,217 @@ 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]) - } + existing_rates = ElectricityRatesCoordinatorResult(period_from, create_rate_data(period_from, period_to, [2, 4])) dispatches = { "planned": [], "completed": [] } with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") - retrieved_rates = await async_refresh_electricity_rates_data( + retrieved_rates: ElectricityRatesCoordinatorResult = await async_refresh_electricity_rates_data( current, client, account_info, + mpan, + serial_number, + True, + False, 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]) +@pytest.mark.parametrize("existing_rates",[ + (None), + (ElectricityRatesCoordinatorResult(period_from, [])), + (ElectricityRatesCoordinatorResult(period_from, None)), +]) +async def test_when_existing_rates_is_none_then_rates_retrieved(existing_rates): + 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 - account_info = get_account_info() - existing_rates = None - expected_retrieved_rates = { - mpan: expected_rates - } - dispatches = { "planned": [], "completed": [] } - - with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): - client = OctopusEnergyApiClient("NOT_REAL") - retrieved_rates = await async_refresh_electricity_rates_data( - current, - client, - account_info, - existing_rates, - dispatches - ) - - 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) - -@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]) - 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_retrieved_rates = ElectricityRatesCoordinatorResult(current, expected_rates) dispatches = { "planned": [], "completed": [] } with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") - retrieved_rates = await async_refresh_electricity_rates_data( + retrieved_rates: ElectricityRatesCoordinatorResult = await async_refresh_electricity_rates_data( current, client, account_info, + mpan, + serial_number, + True, + False, existing_rates, - dispatches + dispatches, + fire_event ) - assert retrieved_rates == expected_retrieved_rates + assert retrieved_rates is not None + assert retrieved_rates.last_retrieved == expected_retrieved_rates.last_retrieved + assert retrieved_rates.rates == expected_retrieved_rates.rates assert rates_returned == True - + 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_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]) - } - expected_retrieved_rates = { - mpan: expected_rates - } + existing_rates = ElectricityRatesCoordinatorResult(period_to - timedelta(days=60), create_rate_data(period_from - timedelta(days=60), period_to - timedelta(days=60), [2, 4])) + expected_retrieved_rates = ElectricityRatesCoordinatorResult(current, expected_rates) dispatches = { "planned": [], "completed": [] } with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") - retrieved_rates = await async_refresh_electricity_rates_data( + retrieved_rates: ElectricityRatesCoordinatorResult = await async_refresh_electricity_rates_data( current, client, account_info, + mpan, + serial_number, + True, + False, existing_rates, - dispatches + dispatches, + fire_event ) - assert retrieved_rates == expected_retrieved_rates + assert retrieved_rates is not None + assert retrieved_rates.last_retrieved == expected_retrieved_rates.last_retrieved + assert retrieved_rates.rates == expected_retrieved_rates.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]) +@pytest.mark.parametrize("is_export_meter",[ + (True), + (False), +]) +async def test_when_dispatched_rates_provided_then_rates_are_adjusted_if_meter_is_export(is_export_meter: bool): + 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 + existing_rates = None + expected_retrieved_rates = ElectricityRatesCoordinatorResult(current, expected_rates) + + expected_dispatch_start = (current + timedelta(hours=2)).replace(second=0, microsecond=0) + expected_dispatch_end = expected_dispatch_start + timedelta(minutes=90) + dispatches = { + "planned": [{ + "start": expected_dispatch_start, + "end": expected_dispatch_end, + "source": "smart-charge" + }], + "completed": [] } - 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"), - "source": "smart-charge" - }], "completed": [] } with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") - retrieved_rates = await async_refresh_electricity_rates_data( + retrieved_rates: ElectricityRatesCoordinatorResult = await async_refresh_electricity_rates_data( current, client, account_info, + mpan, + serial_number, + True, + is_export_meter, existing_rates, - dispatches + dispatches, + fire_event ) - assert len(retrieved_rates) == len(expected_retrieved_rates) - assert mpan in retrieved_rates - assert len(retrieved_rates[mpan]) == len(expected_retrieved_rates[mpan]) + assert retrieved_rates is not None + assert retrieved_rates.last_retrieved == expected_retrieved_rates.last_retrieved + + assert len(retrieved_rates.rates) == len(expected_retrieved_rates.rates) - for index in range(len(retrieved_rates[mpan])): - expected_rate = expected_retrieved_rates[mpan][index] - actual_rate = retrieved_rates[mpan][index] + number_of_intelligent_rates = 0 + expected_number_of_intelligent_rates = 0 if is_export_meter else 3 + for index in range(len(retrieved_rates.rates)): + expected_rate = expected_retrieved_rates.rates[index] + actual_rate = retrieved_rates.rates[index] - if (index == 5 or index == 6): + if is_export_meter == False and 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 + number_of_intelligent_rates = number_of_intelligent_rates + 1 else: assert "is_intelligent_adjusted" not in actual_rate assert expected_rate == actual_rate assert rates_returned == True + assert number_of_intelligent_rates == expected_number_of_intelligent_rates + + 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,23 +368,31 @@ 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 - } + existing_rates = ElectricityRatesCoordinatorResult(period_from, expected_rates) dispatches = { "planned": [], "completed": [] } with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates): client = OctopusEnergyApiClient("NOT_REAL") - retrieved_rates = await async_refresh_electricity_rates_data( + retrieved_rates: ElectricityRatesCoordinatorResult = await async_refresh_electricity_rates_data( current, client, account_info, + mpan, + serial_number, + True, + False, 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 retrieved_rates == existing_rates + 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_electricity_standing_charge_data.py b/tests/unit/coordinators/test_async_refresh_electricity_standing_charge_data.py new file mode 100644 index 00000000..56e1164e --- /dev/null +++ b/tests/unit/coordinators/test_async_refresh_electricity_standing_charge_data.py @@ -0,0 +1,248 @@ +from datetime import datetime, timedelta +import pytest +import mock + +from unit import (create_rate_data) + +from custom_components.octopus_energy.api_client import OctopusEnergyApiClient +from custom_components.octopus_energy.coordinators.electricity_standing_charges import ElectricityStandingChargeCoordinatorResult, async_refresh_electricity_standing_charges_data + +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") +period_to = datetime.strptime("2023-07-15T00:00:00+01:00", "%Y-%m-%dT%H:%M:%S%z") + +tariff_code = "E-1R-SUPER-GREEN-24M-21-07-30-A" +mpan = "1234567890" +serial_number = "abcdefgh" + +def get_account_info(is_active_agreement = True): + return { + "electricity_meter_points": [ + { + "mpan": mpan, + "meters": [ + { + "serial_number": serial_number, + "is_export": False, + "is_smart_meter": True, + "device_id": "", + "manufacturer": "", + "model": "", + "firmware": "" + } + ], + "agreements": [ + { + "valid_from": "2023-07-01T00:00:00+01:00" if is_active_agreement else "2023-08-01T00:00:00+01:00", + "valid_to": "2023-08-01T00:00:00+01:00" if is_active_agreement else "2023-09-01T00:00:00+01:00", + "tariff_code": tariff_code, + "product": "SUPER-GREEN-24M-21-07-30" + } + ] + } + ] + } + +@pytest.mark.asyncio +async def test_when_account_info_is_none_then_existing_standing_charge_returned(): + expected_standing_charge = { + "valid_from": period_from, + "valid_to": period_to, + "value_inc_vat": 0.30 + } + standing_charge_returned = False + async def async_mocked_get_electricity_standing_charge(*args, **kwargs): + nonlocal standing_charge_returned + standing_charge_returned = True + return expected_standing_charge + + account_info = None + existing_standing_charge = ElectricityStandingChargeCoordinatorResult(period_from, create_rate_data(period_from, period_to, [2, 4])) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_standing_charge=async_mocked_get_electricity_standing_charge): + client = OctopusEnergyApiClient("NOT_REAL") + retrieved_standing_charge: ElectricityStandingChargeCoordinatorResult = await async_refresh_electricity_standing_charges_data( + current, + client, + account_info, + mpan, + serial_number, + existing_standing_charge + ) + + assert retrieved_standing_charge == existing_standing_charge + assert standing_charge_returned == False + +@pytest.mark.asyncio +async def test_when_no_active_standing_charge_then_none_returned(): + expected_standing_charge = { + "valid_from": period_from, + "valid_to": period_to, + "value_inc_vat": 0.30 + } + standing_charge_returned = False + async def async_mocked_get_electricity_standing_charge(*args, **kwargs): + nonlocal standing_charge_returned + standing_charge_returned = True + return expected_standing_charge + + account_info = get_account_info(False) + existing_standing_charge = ElectricityStandingChargeCoordinatorResult(period_from, create_rate_data(period_from, period_to, [2, 4])) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_standing_charge=async_mocked_get_electricity_standing_charge): + client = OctopusEnergyApiClient("NOT_REAL") + retrieved_standing_charge: ElectricityStandingChargeCoordinatorResult = await async_refresh_electricity_standing_charges_data( + current, + client, + account_info, + mpan, + serial_number, + existing_standing_charge + ) + + assert retrieved_standing_charge is None + assert standing_charge_returned == False + +@pytest.mark.asyncio +async def test_when_current_is_not_thirty_minutes_then_existing_standing_charge_returned(): + for minute in range(60): + if minute == 0 or minute == 30: + continue + + current = datetime.strptime("2023-07-14T10:30:01+01:00", "%Y-%m-%dT%H:%M:%S%z").replace(minute=minute) + expected_standing_charge = { + "valid_from": period_from, + "valid_to": period_to, + "value_inc_vat": 0.30 + } + standing_charge_returned = False + async def async_mocked_get_electricity_standing_charge(*args, **kwargs): + nonlocal standing_charge_returned + standing_charge_returned = True + return expected_standing_charge + + account_info = get_account_info() + existing_standing_charge = ElectricityStandingChargeCoordinatorResult(period_from, { + "valid_from": period_from, + "valid_to": period_to, + "value_inc_vat": 0.10 + }) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_standing_charge=async_mocked_get_electricity_standing_charge): + client = OctopusEnergyApiClient("NOT_REAL") + retrieved_standing_charge: ElectricityStandingChargeCoordinatorResult = await async_refresh_electricity_standing_charges_data( + current, + client, + account_info, + mpan, + serial_number, + existing_standing_charge + ) + + assert retrieved_standing_charge == existing_standing_charge + assert standing_charge_returned == False + + +@pytest.mark.asyncio +@pytest.mark.parametrize("existing_standing_charge",[ + (None), + (ElectricityStandingChargeCoordinatorResult(period_from, [])), + (ElectricityStandingChargeCoordinatorResult(period_from, None)), +]) +async def test_when_existing_standing_charge_is_none_then_standing_charge_retrieved(existing_standing_charge): + expected_period_from = current.replace(hour=0, minute=0, second=0, microsecond=0) + expected_period_to = expected_period_from + timedelta(days=1) + expected_standing_charge = { + "valid_from": period_from, + "valid_to": period_to, + "value_inc_vat": 0.30 + } + standing_charge_returned = False + requested_period_from = None + requested_period_to = None + async def async_mocked_get_electricity_standing_charge(*args, **kwargs): + nonlocal requested_period_from, requested_period_to, standing_charge_returned, expected_standing_charge + + requested_client, requested_tariff_code, requested_period_from, requested_period_to = args + standing_charge_returned = True + return expected_standing_charge + + account_info = get_account_info() + expected_retrieved_standing_charge = ElectricityStandingChargeCoordinatorResult(current, expected_standing_charge) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_standing_charge=async_mocked_get_electricity_standing_charge): + client = OctopusEnergyApiClient("NOT_REAL") + retrieved_standing_charge: ElectricityStandingChargeCoordinatorResult = await async_refresh_electricity_standing_charges_data( + current, + client, + account_info, + mpan, + serial_number, + existing_standing_charge + ) + + assert retrieved_standing_charge is not None + assert retrieved_standing_charge.last_retrieved == expected_retrieved_standing_charge.last_retrieved + assert retrieved_standing_charge.standing_charge == expected_retrieved_standing_charge.standing_charge + assert standing_charge_returned == True + assert requested_period_from == expected_period_from + assert requested_period_to == expected_period_to + +@pytest.mark.asyncio +async def test_when_existing_standing_charge_is_old_then_standing_charge_retrieved(): + expected_standing_charge = { + "valid_from": period_from, + "valid_to": period_to, + "value_inc_vat": 0.30 + } + standing_charge_returned = False + async def async_mocked_get_electricity_standing_charge(*args, **kwargs): + nonlocal standing_charge_returned + standing_charge_returned = True + return expected_standing_charge + + account_info = get_account_info() + existing_standing_charge = ElectricityStandingChargeCoordinatorResult(period_to - timedelta(days=60), create_rate_data(period_from - timedelta(days=60), period_to - timedelta(days=60), [2, 4])) + expected_retrieved_standing_charge = ElectricityStandingChargeCoordinatorResult(current, expected_standing_charge) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_standing_charge=async_mocked_get_electricity_standing_charge): + client = OctopusEnergyApiClient("NOT_REAL") + retrieved_standing_charge: ElectricityStandingChargeCoordinatorResult = await async_refresh_electricity_standing_charges_data( + current, + client, + account_info, + mpan, + serial_number, + existing_standing_charge + ) + + assert retrieved_standing_charge is not None + assert retrieved_standing_charge.last_retrieved == expected_retrieved_standing_charge.last_retrieved + assert retrieved_standing_charge.standing_charge == expected_retrieved_standing_charge.standing_charge + assert standing_charge_returned == True + +@pytest.mark.asyncio +async def test_when_standing_charge_not_retrieved_then_existing_standing_charge_returned(): + expected_standing_charge = create_rate_data(period_from, period_to, [1, 2, 3, 4]) + standing_charge_returned = False + async def async_mocked_get_electricity_standing_charge(*args, **kwargs): + nonlocal standing_charge_returned + standing_charge_returned = True + return None + + account_info = get_account_info() + existing_standing_charge = ElectricityStandingChargeCoordinatorResult(period_from, expected_standing_charge) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_standing_charge=async_mocked_get_electricity_standing_charge): + client = OctopusEnergyApiClient("NOT_REAL") + retrieved_standing_charge: ElectricityStandingChargeCoordinatorResult = await async_refresh_electricity_standing_charges_data( + current, + client, + account_info, + mpan, + serial_number, + existing_standing_charge + ) + + assert retrieved_standing_charge == existing_standing_charge + assert standing_charge_returned == True \ 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..ea1d2181 100644 --- a/tests/unit/coordinators/test_async_refresh_gas_rates_data.py +++ b/tests/unit/coordinators/test_async_refresh_gas_rates_data.py @@ -5,7 +5,8 @@ from unit import (create_rate_data) 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.coordinators.gas_rates import GasRatesCoordinatorResult, 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,19 @@ 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 "serial_number" in raised_events[expected_event_name] + assert raised_events[expected_event_name]["serial_number"] == serial_number + 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,10 +66,14 @@ 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]) - } + existing_rates = GasRatesCoordinatorResult(period_from, create_rate_data(period_from, period_to, [2, 4])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_rates=async_mocked_get_gas_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -63,14 +81,18 @@ async def async_mocked_get_gas_rates(*args, **kwargs): current, client, account_info, - existing_rates + mprn, + serial_number, + 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(): +async def test_when_no_active_rates_then_none_returned(): expected_rates = create_rate_data(period_from, period_to, [1, 2]) rates_returned = False async def async_mocked_get_gas_rates(*args, **kwargs): @@ -78,10 +100,14 @@ 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]) - } + existing_rates = GasRatesCoordinatorResult(period_from, create_rate_data(period_from, period_to, [2, 4])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_rates=async_mocked_get_gas_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -89,11 +115,15 @@ async def async_mocked_get_gas_rates(*args, **kwargs): current, client, account_info, - existing_rates + mprn, + serial_number, + existing_rates, + fire_event ) - assert retrieved_rates == {} + assert retrieved_rates == None 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,10 +139,14 @@ 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]) - } + existing_rates = GasRatesCoordinatorResult(period_from, create_rate_data(period_from, period_to, [2, 4])) with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_rates=async_mocked_get_gas_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -120,15 +154,26 @@ async def async_mocked_get_gas_rates(*args, **kwargs): current, client, account_info, - existing_rates + mprn, + serial_number, + 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]) +@pytest.mark.parametrize("existing_rates",[ + (None), + (GasRatesCoordinatorResult(period_from, [])), + (GasRatesCoordinatorResult(period_from, None)), +]) +async def test_when_existing_rates_is_none_then_rates_retrieved(existing_rates): + 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,40 +184,14 @@ async def async_mocked_get_gas_rates(*args, **kwargs): rates_returned = True return expected_rates - account_info = get_account_info() - existing_rates = None - expected_retrieved_rates = { - mprn: expected_rates - } - - with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_rates=async_mocked_get_gas_rates): - client = OctopusEnergyApiClient("NOT_REAL") - retrieved_rates = await async_refresh_gas_rates_data( - current, - client, - account_info, - existing_rates - ) - - 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) - -@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]) - 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 = { - mprn: expected_rates - } + expected_retrieved_rates = GasRatesCoordinatorResult(current, expected_rates) with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_rates=async_mocked_get_gas_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -180,28 +199,45 @@ async def async_mocked_get_gas_rates(*args, **kwargs): current, client, account_info, - existing_rates + mprn, + serial_number, + existing_rates, + fire_event ) - assert retrieved_rates == expected_retrieved_rates + assert retrieved_rates is not None + assert retrieved_rates.last_retrieved == expected_retrieved_rates.last_retrieved + assert retrieved_rates.rates == expected_retrieved_rates.rates assert rates_returned == True + 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_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]) - } - expected_retrieved_rates = { - mprn: expected_rates - } + existing_rates = GasRatesCoordinatorResult(period_from, create_rate_data(period_from - timedelta(days=60), period_to - timedelta(days=60), [2, 4])) + expected_retrieved_rates = GasRatesCoordinatorResult(current, expected_rates) with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_rates=async_mocked_get_gas_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -209,11 +245,21 @@ async def async_mocked_get_gas_rates(*args, **kwargs): current, client, account_info, - existing_rates + mprn, + serial_number, + existing_rates, + fire_event ) - assert retrieved_rates == expected_retrieved_rates + assert retrieved_rates is not None + assert retrieved_rates.last_retrieved == expected_retrieved_rates.last_retrieved + assert retrieved_rates.rates == expected_retrieved_rates.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,10 +270,14 @@ 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 - } + existing_rates = GasRatesCoordinatorResult(period_from, expected_rates) with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_rates=async_mocked_get_gas_rates): client = OctopusEnergyApiClient("NOT_REAL") @@ -235,10 +285,14 @@ async def async_mocked_get_gas_rates(*args, **kwargs): current, client, account_info, - existing_rates + mprn, + serial_number, + 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 retrieved_rates.last_retrieved == existing_rates.last_retrieved + assert retrieved_rates.rates == existing_rates.rates + 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_standing_charge_data.py b/tests/unit/coordinators/test_async_refresh_gas_standing_charge_data.py new file mode 100644 index 00000000..361f9452 --- /dev/null +++ b/tests/unit/coordinators/test_async_refresh_gas_standing_charge_data.py @@ -0,0 +1,247 @@ +from datetime import datetime, timedelta +import pytest +import mock + +from unit import (create_rate_data) + +from custom_components.octopus_energy.api_client import OctopusEnergyApiClient +from custom_components.octopus_energy.coordinators.gas_standing_charges import GasStandingChargeCoordinatorResult, async_refresh_gas_standing_charges_data + +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") +period_to = datetime.strptime("2023-07-15T00:00:00+01:00", "%Y-%m-%dT%H:%M:%S%z") + +tariff_code = "E-1R-SUPER-GREEN-24M-21-07-30-A" +mprn = "1234567890" +serial_number = "abcdefgh" + +def get_account_info(is_active_agreement = True): + return { + "gas_meter_points": [ + { + "mprn": mprn, + "meters": [ + { + "serial_number": serial_number, + "is_export": False, + "is_smart_meter": True, + "device_id": "", + "manufacturer": "", + "model": "", + "firmware": "" + } + ], + "agreements": [ + { + "valid_from": "2023-07-01T00:00:00+01:00" if is_active_agreement else "2023-08-01T00:00:00+01:00", + "valid_to": "2023-08-01T00:00:00+01:00" if is_active_agreement else "2023-09-01T00:00:00+01:00", + "tariff_code": tariff_code, + "product": "SUPER-GREEN-24M-21-07-30" + } + ] + } + ] + } + +@pytest.mark.asyncio +async def test_when_account_info_is_none_then_existing_standing_charge_returned(): + expected_standing_charge = { + "valid_from": period_from, + "valid_to": period_to, + "value_inc_vat": 0.30 + } + standing_charge_returned = False + async def async_mocked_get_gas_standing_charge(*args, **kwargs): + nonlocal standing_charge_returned + standing_charge_returned = True + return expected_standing_charge + + account_info = None + existing_standing_charge = GasStandingChargeCoordinatorResult(period_from, create_rate_data(period_from, period_to, [2, 4])) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_standing_charge=async_mocked_get_gas_standing_charge): + client = OctopusEnergyApiClient("NOT_REAL") + retrieved_standing_charge: GasStandingChargeCoordinatorResult = await async_refresh_gas_standing_charges_data( + current, + client, + account_info, + mprn, + serial_number, + existing_standing_charge + ) + + assert retrieved_standing_charge == existing_standing_charge + assert standing_charge_returned == False + +@pytest.mark.asyncio +async def test_when_no_active_standing_charge_then_none_returned(): + expected_standing_charge = { + "valid_from": period_from, + "valid_to": period_to, + "value_inc_vat": 0.30 + } + standing_charge_returned = False + async def async_mocked_get_gas_standing_charge(*args, **kwargs): + nonlocal standing_charge_returned + standing_charge_returned = True + return expected_standing_charge + + account_info = get_account_info(False) + existing_standing_charge = GasStandingChargeCoordinatorResult(period_from, create_rate_data(period_from, period_to, [2, 4])) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_standing_charge=async_mocked_get_gas_standing_charge): + client = OctopusEnergyApiClient("NOT_REAL") + retrieved_standing_charge: GasStandingChargeCoordinatorResult = await async_refresh_gas_standing_charges_data( + current, + client, + account_info, + mprn, + serial_number, + existing_standing_charge + ) + + assert retrieved_standing_charge is None + assert standing_charge_returned == False + +@pytest.mark.asyncio +async def test_when_current_is_not_thirty_minutes_then_existing_standing_charge_returned(): + for minute in range(60): + if minute == 0 or minute == 30: + continue + + current = datetime.strptime("2023-07-14T10:30:01+01:00", "%Y-%m-%dT%H:%M:%S%z").replace(minute=minute) + expected_standing_charge = { + "valid_from": period_from, + "valid_to": period_to, + "value_inc_vat": 0.30 + } + standing_charge_returned = False + async def async_mocked_get_gas_standing_charge(*args, **kwargs): + nonlocal standing_charge_returned + standing_charge_returned = True + return expected_standing_charge + + account_info = get_account_info() + existing_standing_charge = GasStandingChargeCoordinatorResult(period_from, { + "valid_from": period_from, + "valid_to": period_to, + "value_inc_vat": 0.10 + }) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_standing_charge=async_mocked_get_gas_standing_charge): + client = OctopusEnergyApiClient("NOT_REAL") + retrieved_standing_charge: GasStandingChargeCoordinatorResult = await async_refresh_gas_standing_charges_data( + current, + client, + account_info, + mprn, + serial_number, + existing_standing_charge + ) + + assert retrieved_standing_charge == existing_standing_charge + assert standing_charge_returned == False + +@pytest.mark.asyncio +@pytest.mark.parametrize("existing_standing_charge",[ + (None), + (GasStandingChargeCoordinatorResult(period_from, [])), + (GasStandingChargeCoordinatorResult(period_from, None)), +]) +async def test_when_existing_standing_charge_is_none_then_standing_charge_retrieved(existing_standing_charge): + expected_period_from = current.replace(hour=0, minute=0, second=0, microsecond=0) + expected_period_to = expected_period_from + timedelta(days=1) + expected_standing_charge = { + "valid_from": period_from, + "valid_to": period_to, + "value_inc_vat": 0.30 + } + standing_charge_returned = False + requested_period_from = None + requested_period_to = None + async def async_mocked_get_gas_standing_charge(*args, **kwargs): + nonlocal requested_period_from, requested_period_to, standing_charge_returned, expected_standing_charge + + requested_client, requested_tariff_code, requested_period_from, requested_period_to = args + standing_charge_returned = True + return expected_standing_charge + + account_info = get_account_info() + expected_retrieved_standing_charge = GasStandingChargeCoordinatorResult(current, expected_standing_charge) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_standing_charge=async_mocked_get_gas_standing_charge): + client = OctopusEnergyApiClient("NOT_REAL") + retrieved_standing_charge: GasStandingChargeCoordinatorResult = await async_refresh_gas_standing_charges_data( + current, + client, + account_info, + mprn, + serial_number, + existing_standing_charge + ) + + assert retrieved_standing_charge is not None + assert retrieved_standing_charge.last_retrieved == expected_retrieved_standing_charge.last_retrieved + assert retrieved_standing_charge.standing_charge == expected_retrieved_standing_charge.standing_charge + assert standing_charge_returned == True + assert requested_period_from == expected_period_from + assert requested_period_to == expected_period_to + +@pytest.mark.asyncio +async def test_when_existing_standing_charge_is_old_then_standing_charge_retrieved(): + expected_standing_charge = { + "valid_from": period_from, + "valid_to": period_to, + "value_inc_vat": 0.30 + } + standing_charge_returned = False + async def async_mocked_get_gas_standing_charge(*args, **kwargs): + nonlocal standing_charge_returned + standing_charge_returned = True + return expected_standing_charge + + account_info = get_account_info() + existing_standing_charge = GasStandingChargeCoordinatorResult(period_to - timedelta(days=60), create_rate_data(period_from - timedelta(days=60), period_to - timedelta(days=60), [2, 4])) + expected_retrieved_standing_charge = GasStandingChargeCoordinatorResult(current, expected_standing_charge) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_standing_charge=async_mocked_get_gas_standing_charge): + client = OctopusEnergyApiClient("NOT_REAL") + retrieved_standing_charge: GasStandingChargeCoordinatorResult = await async_refresh_gas_standing_charges_data( + current, + client, + account_info, + mprn, + serial_number, + existing_standing_charge + ) + + assert retrieved_standing_charge is not None + assert retrieved_standing_charge.last_retrieved == expected_retrieved_standing_charge.last_retrieved + assert retrieved_standing_charge.standing_charge == expected_retrieved_standing_charge.standing_charge + assert standing_charge_returned == True + +@pytest.mark.asyncio +async def test_when_standing_charge_not_retrieved_then_existing_standing_charge_returned(): + expected_standing_charge = create_rate_data(period_from, period_to, [1, 2, 3, 4]) + standing_charge_returned = False + async def async_mocked_get_gas_standing_charge(*args, **kwargs): + nonlocal standing_charge_returned + standing_charge_returned = True + return None + + account_info = get_account_info() + existing_standing_charge = GasStandingChargeCoordinatorResult(period_from, expected_standing_charge) + + with mock.patch.multiple(OctopusEnergyApiClient, async_get_gas_standing_charge=async_mocked_get_gas_standing_charge): + client = OctopusEnergyApiClient("NOT_REAL") + retrieved_standing_charge: GasStandingChargeCoordinatorResult = await async_refresh_gas_standing_charges_data( + current, + client, + account_info, + mprn, + serial_number, + existing_standing_charge + ) + + assert retrieved_standing_charge == existing_standing_charge + assert standing_charge_returned == True \ 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..6f73b223 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,13 +8,34 @@ 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 +sensor_serial_number = "123456" + +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 "serial_number" in raised_events[expected_event_name] + assert raised_events[expected_event_name]["serial_number"] == sensor_serial_number + 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 client = OctopusEnergyApiClient("NOT_REAL") sensor_identifier = "ABC123" - sensor_serial_number = "123456" is_electricity = False tariff_code = "AB-123" is_smart_meter = True @@ -29,6 +51,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 +72,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), @@ -60,7 +91,6 @@ async def test_when_now_is_at_30_minute_mark_and_previous_data_is_in_requested_p client = OctopusEnergyApiClient("NOT_REAL") sensor_identifier = "ABC123" - sensor_serial_number = "123456" is_electricity = False tariff_code = "AB-123" is_smart_meter = True @@ -76,6 +106,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 +123,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,11 +157,16 @@ 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") sensor_identifier = "ABC123" - sensor_serial_number = "123456" is_electricity = False tariff_code = "AB-123" is_smart_meter = True @@ -157,7 +201,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 +229,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,11 +262,16 @@ 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") sensor_identifier = "ABC123" - sensor_serial_number = "123456" is_electricity = True tariff_code = "AB-123" is_smart_meter = True @@ -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,12 +356,17 @@ 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") sensor_identifier = "ABC123" - sensor_serial_number = "123456" is_electricity = False tariff_code = "AB-123" is_smart_meter = True @@ -331,12 +403,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,12 +424,17 @@ 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") sensor_identifier = "ABC123" - sensor_serial_number = "123456" is_electricity = True tariff_code = "AB-123" is_smart_meter = True @@ -395,8 +475,83 @@ 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 + + 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" + 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 \ No newline at end of file + 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 diff --git a/tests/unit/intelligent/test_has_intelligent_tariff.py b/tests/unit/intelligent/test_has_intelligent_tariff.py new file mode 100644 index 00000000..15e7bbc6 --- /dev/null +++ b/tests/unit/intelligent/test_has_intelligent_tariff.py @@ -0,0 +1,79 @@ +import pytest +from datetime import datetime + +from custom_components.octopus_energy.intelligent import has_intelligent_tariff + +current = datetime.strptime("2023-07-14T10:30:01+01:00", "%Y-%m-%dT%H:%M:%S%z") + +def get_account_info(tariff_code: str, is_active_agreement = True): + return { + "electricity_meter_points": [ + { + "mpan": "test-mpan", + "meters": [ + { + "serial_number": "test-serial-number", + "is_export": False, + "is_smart_meter": True, + "device_id": "", + "manufacturer": "", + "model": "", + "firmware": "" + } + ], + "agreements": [ + { + "valid_from": "2023-07-01T00:00:00+01:00" if is_active_agreement else "2023-08-01T00:00:00+01:00", + "valid_to": "2023-08-01T00:00:00+01:00" if is_active_agreement else "2023-09-01T00:00:00+01:00", + "tariff_code": tariff_code, + "product": "SUPER-GREEN-24M-21-07-30" + } + ] + } + ] + } + +@pytest.mark.asyncio +async def test_when_account_info_is_none_then_false_returned(): + account_info = None + + # Act + assert has_intelligent_tariff(current, account_info) == False + +@pytest.mark.asyncio +async def test_when_account_info_has_no_electricity_meters_then_false_returned(): + account_info = { + "electricity_meter_points": [] + } + + # Act + assert has_intelligent_tariff(current, account_info) == False + +@pytest.mark.asyncio +@pytest.mark.parametrize("tariff_code",[ + ("E-1R-INTELLI-VAR-22-10-14-C".upper()), + ("E-1R-INTELLI-VAR-22-10-14-C".lower()), +]) +async def test_when_tariff_code_is_and_not_active_then_true_returned(tariff_code: str): + account_info = get_account_info(tariff_code, False) + + # Act + assert has_intelligent_tariff(current, account_info) == False + +@pytest.mark.asyncio +@pytest.mark.parametrize("tariff_code",[ + ("E-1R-INTELLI-VAR-22-10-14-C".upper()), + ("E-1R-INTELLI-VAR-22-10-14-C".lower()), +]) +async def test_when_tariff_code_is_valid_and_active_then_true_returned(tariff_code: str): + account_info = get_account_info(tariff_code, True) + + # Act + assert has_intelligent_tariff(current, account_info) == True + +@pytest.mark.asyncio +async def test_when_tariff_is_invalid_then_false_returned(): + account_info = get_account_info("invalid-tariff-code", True) + + # Act + assert has_intelligent_tariff(current, account_info) == False \ No newline at end of file diff --git a/tests/unit/utils/test_get_current_consumption_delta.py b/tests/unit/utils/test_get_current_consumption_delta.py index b3f93f5f..48c0e421 100644 --- a/tests/unit/utils/test_get_current_consumption_delta.py +++ b/tests/unit/utils/test_get_current_consumption_delta.py @@ -1,7 +1,6 @@ from datetime import (datetime) import pytest -from unit import (create_rate_data) from custom_components.octopus_energy.utils.consumption import get_current_consumption_delta @pytest.mark.asyncio diff --git a/tests/unit/utils/test_get_current_rate_information.py b/tests/unit/utils/test_get_current_rate_information.py index 72be674a..777ec437 100644 --- a/tests/unit/utils/test_get_current_rate_information.py +++ b/tests/unit/utils/test_get_current_rate_information.py @@ -3,6 +3,7 @@ from unit import (create_rate_data) from custom_components.octopus_energy.utils.rate_information import get_current_rate_information +from custom_components.octopus_energy.api_client import rates_to_thirty_minute_increments @pytest.mark.asyncio async def test_when_target_has_no_rates_and_gmt_then_no_rate_information_is_returned(): @@ -187,4 +188,76 @@ async def test_when_all_rates_identical_costs_then_rate_information_is_returned( assert rate_information["max_rate_today"] == expected_rate assert "average_rate_today" in rate_information - assert rate_information["average_rate_today"] == total_rate_value / 48 \ No newline at end of file + assert rate_information["average_rate_today"] == total_rate_value / 48 + +# Covering https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/issues/441 +@pytest.mark.asyncio +@pytest.mark.parametrize("now",[ + (datetime.strptime("2023-10-06T10:50:00+01:00", "%Y-%m-%dT%H:%M:%S%z")), + (datetime.strptime("2023-10-06T09:50:00Z", "%Y-%m-%dT%H:%M:%S%z")), +]) +async def test_when_agile_rates_then_rate_information_is_returned(now: datetime): + # Arrange + rate_data = rates_to_thirty_minute_increments( + { + "results": [ + { + "value_inc_vat": 2.0, + "valid_from": "2023-10-06T11:30:00Z", + "valid_to": "2023-10-06T12:00:00Z" + }, + { + "value_inc_vat": 3.02, + "valid_from": "2023-10-06T11:00:00Z", + "valid_to": "2023-10-06T11:30:00Z" + }, + { + "value_inc_vat": 3.43, + "valid_from": "2023-10-06T10:30:00Z", + "valid_to": "2023-10-06T11:00:00Z" + }, + { + "value_inc_vat": 4.18, + "valid_from": "2023-10-06T10:00:00Z", + "valid_to": "2023-10-06T10:30:00Z" + }, + { + "value_inc_vat": 6.19, + "valid_from": "2023-10-06T09:30:00Z", + "valid_to": "2023-10-06T10:00:00Z" + }, + { + "value_inc_vat": 6.74, + "valid_from": "2023-10-06T09:00:00Z", + "valid_to": "2023-10-06T09:30:00Z" + }, + { + "value_inc_vat": 7.6, + "valid_from": "2023-10-06T08:30:00Z", + "valid_to": "2023-10-06T09:00:00Z" + }, + ] + }, + datetime.strptime("2023-10-06T09:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), + datetime.strptime("2023-10-06T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), + "test" + ) + expected_current_rate = 6.19 + + # Act + rate_information = get_current_rate_information(rate_data, now) + + # Assert + assert rate_information is not None + + assert "current_rate" in rate_information + assert rate_information["current_rate"]["value_inc_vat"] == expected_current_rate + assert rate_information["current_rate"]["valid_from"] == datetime.strptime("2023-10-06T09:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["current_rate"]["valid_to"] == datetime.strptime("2023-10-06T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + + assert "applicable_rates" in rate_information + assert len(rate_information["applicable_rates"]) == 1 + + assert rate_information["applicable_rates"][0]["value_inc_vat"] == expected_current_rate + assert rate_information["applicable_rates"][0]["valid_from"] == datetime.strptime("2023-10-06T09:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["applicable_rates"][0]["valid_to"] == datetime.strptime("2023-10-06T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") diff --git a/tests/unit/utils/test_get_next_rate_information.py b/tests/unit/utils/test_get_next_rate_information.py index 1febd99a..99a081a7 100644 --- a/tests/unit/utils/test_get_next_rate_information.py +++ b/tests/unit/utils/test_get_next_rate_information.py @@ -1,17 +1,17 @@ -from datetime import (datetime, timedelta) +from datetime import (datetime) import pytest from unit import (create_rate_data) from custom_components.octopus_energy.utils.rate_information import get_next_rate_information +from custom_components.octopus_energy.api_client import rates_to_thirty_minute_increments +from .. import agile_rates @pytest.mark.asyncio async def test_when_target_has_no_rates_and_gmt_then_no_rate_information_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") now = datetime.strptime("2022-03-01T00:00:01Z", "%Y-%m-%dT%H:%M:%S%z") - rate_data = create_rate_data(period_from, period_to, [10, 10, 20, 20, 30, 30]) + rate_data = agile_rates # Act rate_information = get_next_rate_information(rate_data, now) @@ -22,11 +22,9 @@ async def test_when_target_has_no_rates_and_gmt_then_no_rate_information_is_retu @pytest.mark.asyncio async def test_when_target_has_no_rates_and_bst_then_no_rate_information_is_returned(): # Arrange - period_from = datetime.strptime("2022-02-28T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - period_to = datetime.strptime("2022-03-01T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z") now = datetime.strptime("2022-03-02T00:00:01+01:00", "%Y-%m-%dT%H:%M:%S%z") - rate_data = create_rate_data(period_from, period_to, [10, 10, 20, 20, 30, 30]) + rate_data = agile_rates # Act rate_information = get_next_rate_information(rate_data, now) @@ -37,11 +35,9 @@ async def test_when_target_has_no_rates_and_bst_then_no_rate_information_is_retu @pytest.mark.asyncio async def test_when_target_is_at_start_of_rates_and_bst_then_no_rate_information_is_returned(): # Arrange - period_from = datetime.strptime("2022-02-28T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - period_to = datetime.strptime("2022-03-01T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - now = datetime.strptime("2022-02-28T00:00:01+01:00", "%Y-%m-%dT%H:%M:%S%z") + now = datetime.strptime("2022-10-20T23:59:59+01:00", "%Y-%m-%dT%H:%M:%S%z") - rate_data = create_rate_data(period_from, period_to, [10, 10, 20, 20, 30, 30]) + rate_data = agile_rates # Act rate_information = get_next_rate_information(rate_data, now) @@ -67,15 +63,10 @@ async def test_when_next_rate_is_identical_to_current_rate_then_no_rate_informat @pytest.mark.asyncio async def test_when_target_has_rates_and_gmt_then_rate_information_is_returned(): # Arrange - period_from = datetime.strptime("2022-02-27T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - period_to = datetime.strptime("2022-03-02T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - now = datetime.strptime("2022-02-28T01:12:00Z", "%Y-%m-%dT%H:%M:%S%z") + now = datetime.strptime("2022-10-21T03:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - rate_data = create_rate_data(period_from, period_to, [10, 10, 20, 20, 30, 30]) - expected_current_rate = 20 - for rate in rate_data: - if now >= rate["valid_from"] and now <= rate["valid_to"]: - assert expected_current_rate == rate["value_inc_vat"] + rate_data = agile_rates + expected_current_rate = 21.21 # Act rate_information = get_next_rate_information(rate_data, now) @@ -84,33 +75,28 @@ async def test_when_target_has_rates_and_gmt_then_rate_information_is_returned() assert rate_information is not None assert "next_rate" in rate_information - assert rate_information["next_rate"]["value_inc_vat"] == 30 - assert rate_information["next_rate"]["valid_from"] == datetime.strptime("2022-02-28T02:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - assert rate_information["next_rate"]["valid_to"] == datetime.strptime("2022-02-28T03:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["next_rate"]["value_inc_vat"] == expected_current_rate + assert rate_information["next_rate"]["valid_from"] == datetime.strptime("2022-10-21T03:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["next_rate"]["valid_to"] == datetime.strptime("2022-10-21T04:30:00Z", "%Y-%m-%dT%H:%M:%S%z") assert "applicable_rates" in rate_information assert len(rate_information["applicable_rates"]) == 2 - assert rate_information["applicable_rates"][0]["value_inc_vat"] == 30 - assert rate_information["applicable_rates"][0]["valid_from"] == datetime.strptime("2022-02-28T02:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - assert rate_information["applicable_rates"][0]["valid_to"] == datetime.strptime("2022-02-28T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["applicable_rates"][0]["value_inc_vat"] == expected_current_rate + assert rate_information["applicable_rates"][0]["valid_from"] == datetime.strptime("2022-10-21T03:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["applicable_rates"][0]["valid_to"] == datetime.strptime("2022-10-21T04:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - assert rate_information["applicable_rates"][1]["value_inc_vat"] == 30 - assert rate_information["applicable_rates"][1]["valid_from"] == datetime.strptime("2022-02-28T02:30:00Z", "%Y-%m-%dT%H:%M:%S%z") - assert rate_information["applicable_rates"][1]["valid_to"] == datetime.strptime("2022-02-28T03:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["applicable_rates"][1]["value_inc_vat"] == expected_current_rate + assert rate_information["applicable_rates"][1]["valid_from"] == datetime.strptime("2022-10-21T04:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["applicable_rates"][1]["valid_to"] == datetime.strptime("2022-10-21T04:30:00Z", "%Y-%m-%dT%H:%M:%S%z") @pytest.mark.asyncio async def test_when_target_has_rates_and_bst_then_rate_information_is_returned(): # Arrange - period_from = datetime.strptime("2022-02-27T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - period_to = datetime.strptime("2022-03-02T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - now = datetime.strptime("2022-02-28T01:12:00+01:00", "%Y-%m-%dT%H:%M:%S%z") + now = datetime.strptime("2022-10-21T04:00:00+01:00", "%Y-%m-%dT%H:%M:%S%z") - rate_data = create_rate_data(period_from, period_to, [10, 10, 20, 20, 30, 30]) - expected_current_rate = 20 - for rate in rate_data: - if now >= rate["valid_from"] and now <= rate["valid_to"]: - assert expected_current_rate == rate["value_inc_vat"] + rate_data = agile_rates + expected_current_rate = 21.21 # Act rate_information = get_next_rate_information(rate_data, now) @@ -119,33 +105,100 @@ async def test_when_target_has_rates_and_bst_then_rate_information_is_returned() assert rate_information is not None assert "next_rate" in rate_information - assert rate_information["next_rate"]["value_inc_vat"] == 30 - assert rate_information["next_rate"]["valid_from"] == datetime.strptime("2022-02-28T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - assert rate_information["next_rate"]["valid_to"] == datetime.strptime("2022-02-28T02:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["next_rate"]["value_inc_vat"] == expected_current_rate + assert rate_information["next_rate"]["valid_from"] == datetime.strptime("2022-10-21T03:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["next_rate"]["valid_to"] == datetime.strptime("2022-10-21T04:30:00Z", "%Y-%m-%dT%H:%M:%S%z") assert "applicable_rates" in rate_information assert len(rate_information["applicable_rates"]) == 2 - assert rate_information["applicable_rates"][0]["value_inc_vat"] == 30 - assert rate_information["applicable_rates"][0]["valid_from"] == datetime.strptime("2022-02-28T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - assert rate_information["applicable_rates"][0]["valid_to"] == datetime.strptime("2022-02-28T01:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["applicable_rates"][0]["value_inc_vat"] == expected_current_rate + assert rate_information["applicable_rates"][0]["valid_from"] == datetime.strptime("2022-10-21T03:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["applicable_rates"][0]["valid_to"] == datetime.strptime("2022-10-21T04:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - assert rate_information["applicable_rates"][1]["value_inc_vat"] == 30 - assert rate_information["applicable_rates"][1]["valid_from"] == datetime.strptime("2022-02-28T01:30:00Z", "%Y-%m-%dT%H:%M:%S%z") - assert rate_information["applicable_rates"][1]["valid_to"] == datetime.strptime("2022-02-28T02:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["applicable_rates"][1]["value_inc_vat"] == expected_current_rate + assert rate_information["applicable_rates"][1]["valid_from"] == datetime.strptime("2022-10-21T04:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["applicable_rates"][1]["valid_to"] == datetime.strptime("2022-10-21T04:30:00Z", "%Y-%m-%dT%H:%M:%S%z") @pytest.mark.asyncio async def test_when_all_rates_different_then_rate_information_is_returned(): # Arrange - period_from = datetime.strptime("2022-02-27T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - period_to = datetime.strptime("2022-03-02T23:00:00Z", "%Y-%m-%dT%H:%M:%S%z") - now = datetime.strptime("2022-02-28T01:12:00+01:00", "%Y-%m-%dT%H:%M:%S%z") + now = datetime.strptime("2022-10-21T05:30:00+01:00", "%Y-%m-%dT%H:%M:%S%z") + + rate_data = agile_rates + expected_current_rate = 22.449 - rate_data = create_rate_data(period_from, period_to, [10, 20, 30]) - expected_current_rate = 30 - for rate in rate_data: - if now >= rate["valid_from"] and now <= rate["valid_to"]: - assert expected_current_rate == rate["value_inc_vat"] + # Act + rate_information = get_next_rate_information(rate_data, now) + + # Assert + assert rate_information is not None + + assert "next_rate" in rate_information + assert rate_information["next_rate"]["value_inc_vat"] == expected_current_rate + assert rate_information["next_rate"]["valid_from"] == datetime.strptime("2022-10-21T05:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["next_rate"]["valid_to"] == datetime.strptime("2022-10-21T05:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + + assert "applicable_rates" in rate_information + assert len(rate_information["applicable_rates"]) == 1 + + assert rate_information["applicable_rates"][0]["value_inc_vat"] == expected_current_rate + assert rate_information["applicable_rates"][0]["valid_from"] == datetime.strptime("2022-10-21T05:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["applicable_rates"][0]["valid_to"] == datetime.strptime("2022-10-21T05:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + +# Covering https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/issues/441 +@pytest.mark.asyncio +@pytest.mark.parametrize("now",[ + (datetime.strptime("2023-10-06T10:50:00+01:00", "%Y-%m-%dT%H:%M:%S%z")), + (datetime.strptime("2023-10-06T09:50:00Z", "%Y-%m-%dT%H:%M:%S%z")), +]) +async def test_when_agile_rates_then_rate_information_is_returned(now: datetime): + # Arrange + rate_data = rates_to_thirty_minute_increments( + { + "results": [ + { + "value_inc_vat": 2.0, + "valid_from": "2023-10-06T11:30:00Z", + "valid_to": "2023-10-06T12:00:00Z" + }, + { + "value_inc_vat": 3.02, + "valid_from": "2023-10-06T11:00:00Z", + "valid_to": "2023-10-06T11:30:00Z" + }, + { + "value_inc_vat": 3.43, + "valid_from": "2023-10-06T10:30:00Z", + "valid_to": "2023-10-06T11:00:00Z" + }, + { + "value_inc_vat": 4.18, + "valid_from": "2023-10-06T10:00:00Z", + "valid_to": "2023-10-06T10:30:00Z" + }, + { + "value_inc_vat": 6.19, + "valid_from": "2023-10-06T09:30:00Z", + "valid_to": "2023-10-06T10:00:00Z" + }, + { + "value_inc_vat": 6.74, + "valid_from": "2023-10-06T09:00:00Z", + "valid_to": "2023-10-06T09:30:00Z" + }, + { + "value_inc_vat": 7.6, + "valid_from": "2023-10-06T08:30:00Z", + "valid_to": "2023-10-06T09:00:00Z" + }, + ] + }, + datetime.strptime("2023-10-06T09:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), + datetime.strptime("2023-10-06T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), + "test" + ) + expected_current_rate = 4.18 # Act rate_information = get_next_rate_information(rate_data, now) @@ -154,13 +207,13 @@ async def test_when_all_rates_different_then_rate_information_is_returned(): assert rate_information is not None assert "next_rate" in rate_information - assert rate_information["next_rate"]["value_inc_vat"] == 10 - assert rate_information["next_rate"]["valid_from"] == datetime.strptime("2022-02-28T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z") - assert rate_information["next_rate"]["valid_to"] == datetime.strptime("2022-02-28T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["next_rate"]["value_inc_vat"] == expected_current_rate + assert rate_information["next_rate"]["valid_from"] == datetime.strptime("2023-10-06T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["next_rate"]["valid_to"] == datetime.strptime("2023-10-06T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z") assert "applicable_rates" in rate_information assert len(rate_information["applicable_rates"]) == 1 - assert rate_information["applicable_rates"][0]["value_inc_vat"] == 10 - assert rate_information["applicable_rates"][0]["valid_from"] == datetime.strptime("2022-02-28T00:30:00Z", "%Y-%m-%dT%H:%M:%S%z") - assert rate_information["applicable_rates"][0]["valid_to"] == datetime.strptime("2022-02-28T01:00:00Z", "%Y-%m-%dT%H:%M:%S%z") \ No newline at end of file + assert rate_information["applicable_rates"][0]["value_inc_vat"] == expected_current_rate + assert rate_information["applicable_rates"][0]["valid_from"] == datetime.strptime("2023-10-06T10:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["applicable_rates"][0]["valid_to"] == datetime.strptime("2023-10-06T10:30:00Z", "%Y-%m-%dT%H:%M:%S%z") diff --git a/tests/unit/utils/test_get_previous_rate_information.py b/tests/unit/utils/test_get_previous_rate_information.py index 3f3f8fec..475250c4 100644 --- a/tests/unit/utils/test_get_previous_rate_information.py +++ b/tests/unit/utils/test_get_previous_rate_information.py @@ -1,4 +1,5 @@ from datetime import (datetime, timedelta) +from custom_components.octopus_energy.api_client import rates_to_thirty_minute_increments import pytest from unit import (create_rate_data) @@ -163,4 +164,76 @@ async def test_when_all_rates_different_then_rate_information_is_returned(): assert rate_information["applicable_rates"][0]["value_inc_vat"] == 20 assert rate_information["applicable_rates"][0]["valid_from"] == datetime.strptime("2022-02-27T23:30:00Z", "%Y-%m-%dT%H:%M:%S%z") - assert rate_information["applicable_rates"][0]["valid_to"] == datetime.strptime("2022-02-28T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") \ No newline at end of file + assert rate_information["applicable_rates"][0]["valid_to"] == datetime.strptime("2022-02-28T00:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + +# Covering https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/issues/441 +@pytest.mark.asyncio +@pytest.mark.parametrize("now",[ + (datetime.strptime("2023-10-06T10:50:00+01:00", "%Y-%m-%dT%H:%M:%S%z")), + (datetime.strptime("2023-10-06T09:50:00Z", "%Y-%m-%dT%H:%M:%S%z")), +]) +async def test_when_agile_rates_then_rate_information_is_returned(now: datetime): + # Arrange + rate_data = rates_to_thirty_minute_increments( + { + "results": [ + { + "value_inc_vat": 2.0, + "valid_from": "2023-10-06T11:30:00Z", + "valid_to": "2023-10-06T12:00:00Z" + }, + { + "value_inc_vat": 3.02, + "valid_from": "2023-10-06T11:00:00Z", + "valid_to": "2023-10-06T11:30:00Z" + }, + { + "value_inc_vat": 3.43, + "valid_from": "2023-10-06T10:30:00Z", + "valid_to": "2023-10-06T11:00:00Z" + }, + { + "value_inc_vat": 4.18, + "valid_from": "2023-10-06T10:00:00Z", + "valid_to": "2023-10-06T10:30:00Z" + }, + { + "value_inc_vat": 6.19, + "valid_from": "2023-10-06T09:30:00Z", + "valid_to": "2023-10-06T10:00:00Z" + }, + { + "value_inc_vat": 6.74, + "valid_from": "2023-10-06T09:00:00Z", + "valid_to": "2023-10-06T09:30:00Z" + }, + { + "value_inc_vat": 7.6, + "valid_from": "2023-10-06T08:30:00Z", + "valid_to": "2023-10-06T09:00:00Z" + }, + ] + }, + datetime.strptime("2023-10-06T09:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), + datetime.strptime("2023-10-06T12:00:00Z", "%Y-%m-%dT%H:%M:%S%z"), + "test" + ) + expected_current_rate = 6.74 + + # Act + rate_information = get_previous_rate_information(rate_data, now) + + # Assert + assert rate_information is not None + + assert "previous_rate" in rate_information + assert rate_information["previous_rate"]["value_inc_vat"] == expected_current_rate + assert rate_information["previous_rate"]["valid_from"] == datetime.strptime("2023-10-06T09:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["previous_rate"]["valid_to"] == datetime.strptime("2023-10-06T09:30:00Z", "%Y-%m-%dT%H:%M:%S%z") + + assert "applicable_rates" in rate_information + assert len(rate_information["applicable_rates"]) == 1 + + assert rate_information["applicable_rates"][0]["value_inc_vat"] == expected_current_rate + assert rate_information["applicable_rates"][0]["valid_from"] == datetime.strptime("2023-10-06T09:00:00Z", "%Y-%m-%dT%H:%M:%S%z") + assert rate_information["applicable_rates"][0]["valid_to"] == datetime.strptime("2023-10-06T09:30:00Z", "%Y-%m-%dT%H:%M:%S%z")