Skip to content

Commit

Permalink
feat: Updated gas standing charge coordinator to focus on single meter
Browse files Browse the repository at this point in the history
  • Loading branch information
BottlecapDave committed Oct 1, 2023
1 parent 8524005 commit 588e7ed
Show file tree
Hide file tree
Showing 9 changed files with 308 additions and 55 deletions.
3 changes: 1 addition & 2 deletions custom_components/octopus_energy/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@

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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@

from ..api_client import OctopusEnergyApiClient

from . import get_current_electricity_agreement_tariff_codes

_LOGGER = logging.getLogger(__name__)

class ElectricityStandingChargeCoordinatorResult:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,91 +1,107 @@
import logging
from datetime import datetime, timedelta
from custom_components.octopus_energy.utils import get_active_tariff_code

from homeassistant.util.dt import (now, as_utc)
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator
)

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

def get_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"]:
if active_tariff_code is not None and point["mprn"] == target_mprn and meter["serial_number"] == target_serial_number:
return active_tariff_code

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_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["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),
)

await coordinator.async_config_entry_first_refresh()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ 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.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[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

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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ 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.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[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

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,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/octopus_energy/gas/standing_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,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"]
Expand Down
3 changes: 1 addition & 2 deletions custom_components/octopus_energy/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +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_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]
Expand All @@ -200,6 +198,7 @@ async def async_setup_default_sensors(hass: HomeAssistant, entry, async_add_enti
_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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

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
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")
Expand Down Expand Up @@ -187,8 +186,6 @@ async def async_mocked_get_electricity_standing_charge(*args, **kwargs):

@pytest.mark.asyncio
async def test_when_existing_standing_charge_is_old_then_standing_charge_retrieved():
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_standing_charge = {
"valid_from": period_from,
"valid_to": period_to,
Expand Down
Loading

0 comments on commit 588e7ed

Please sign in to comment.