Skip to content

Commit

Permalink
fix: Fixed issue where rate information wasn't retrieved for intellig…
Browse files Browse the repository at this point in the history
…ent tariffs where an intelligent device wasn't available (2 hours dev time)
  • Loading branch information
BottlecapDave committed Aug 4, 2024
1 parent 31d072f commit 50a7bdf
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 7 deletions.
2 changes: 2 additions & 0 deletions custom_components/octopus_energy/api_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1250,6 +1250,8 @@ async def async_get_intelligent_device(self, account_id: str) -> IntelligentDevi
device["chargePointModel"],
float(device["chargePointPowerInKw"]) if "chargePointPowerInKw" in device and device["chargePointPowerInKw"] is not None else None
)
else:
_LOGGER.debug('Skipping intelligent device as id is not available')
else:
_LOGGER.error("Failed to retrieve intelligent device")

Expand Down
17 changes: 12 additions & 5 deletions custom_components/octopus_energy/coordinators/electricity_rates.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from ..const import (
COORDINATOR_REFRESH_IN_SECONDS,
DATA_INTELLIGENT_DEVICE,
DOMAIN,
DATA_CLIENT,
DATA_ELECTRICITY_RATES_COORDINATOR_KEY,
Expand All @@ -30,6 +31,8 @@
from ..intelligent import adjust_intelligent_rates, is_intelligent_product
from ..utils.rate_information import get_unique_rates, has_peak_rates
from ..utils.tariff_cache import async_save_cached_tariff_total_unique_rates
from ..api_client.intelligent_device import IntelligentDevice
from ..api_client.intelligent_dispatches import IntelligentDispatches

_LOGGER = logging.getLogger(__name__)

Expand All @@ -52,8 +55,9 @@ async def async_refresh_electricity_rates_data(
target_serial_number: str,
is_smart_meter: bool,
is_export_meter: bool,
existing_rates_result: ElectricityRatesCoordinatorResult,
dispatches_result: IntelligentDispatchesCoordinatorResult,
existing_rates_result: ElectricityRatesCoordinatorResult | None,
intelligent_device: IntelligentDevice | None,
dispatches_result: IntelligentDispatchesCoordinatorResult | None,
planned_dispatches_supported: bool,
fire_event: Callable[[str, "dict[str, Any]"], None],
tariff_override = None,
Expand All @@ -68,7 +72,8 @@ async def async_refresh_electricity_rates_data(
return None

# We'll calculate the wrong value if we don't have our intelligent dispatches
if is_intelligent_product(tariff.product) and (dispatches_result is None or dispatches_result.dispatches is None):
if is_intelligent_product(tariff.product) and intelligent_device is not None and (dispatches_result is None or dispatches_result.dispatches is None):
_LOGGER.debug("Dispatches not available for intelligent tariff. Using existing rate information")
return existing_rates_result

new_rates = None
Expand Down Expand Up @@ -205,8 +210,9 @@ async def async_update_electricity_rates_data():
client: OctopusEnergyApiClient = hass.data[DOMAIN][account_id][DATA_CLIENT]
account_result = hass.data[DOMAIN][account_id][DATA_ACCOUNT] if DATA_ACCOUNT in hass.data[DOMAIN][account_id] else None
account_info = account_result.account if account_result is not None else None
dispatches: IntelligentDispatchesCoordinatorResult = hass.data[DOMAIN][account_id][DATA_INTELLIGENT_DISPATCHES] if DATA_INTELLIGENT_DISPATCHES in hass.data[DOMAIN][account_id] else None
rates = hass.data[DOMAIN][account_id][key] if key in hass.data[DOMAIN][account_id] else None
intelligent_device: IntelligentDevice | None = hass.data[DOMAIN][account_id][DATA_INTELLIGENT_DEVICE] if DATA_INTELLIGENT_DEVICE in hass.data[DOMAIN][account_id] else None
dispatches: IntelligentDispatchesCoordinatorResult | None = hass.data[DOMAIN][account_id][DATA_INTELLIGENT_DISPATCHES] if DATA_INTELLIGENT_DISPATCHES in hass.data[DOMAIN][account_id] else None
rates: ElectricityRatesCoordinatorResult | None = hass.data[DOMAIN][account_id][key] if key in hass.data[DOMAIN][account_id] else None

hass.data[DOMAIN][account_id][key] = await async_refresh_electricity_rates_data(
current,
Expand All @@ -217,6 +223,7 @@ async def async_update_electricity_rates_data():
is_smart_meter,
is_export_meter,
rates,
intelligent_device,
dispatches,
planned_dispatches_supported,
hass.bus.async_fire,
Expand Down
111 changes: 109 additions & 2 deletions tests/unit/coordinators/test_async_refresh_electricity_rates_data.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime, timedelta
from custom_components.octopus_energy.coordinators.intelligent_dispatches import IntelligentDispatchesCoordinatorResult
import pytest
import mock

Expand All @@ -9,6 +8,8 @@
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, REFRESH_RATE_IN_MINUTES_RATES
from custom_components.octopus_energy.api_client.intelligent_dispatches import IntelligentDispatchItem, IntelligentDispatches
from custom_components.octopus_energy.api_client.intelligent_device import IntelligentDevice
from custom_components.octopus_energy.coordinators.intelligent_dispatches import IntelligentDispatchesCoordinatorResult

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 @@ -112,6 +113,7 @@ def fire_event(name, metadata):
True,
False,
existing_rates,
None,
dispatches_result,
True,
fire_event
Expand Down Expand Up @@ -151,6 +153,7 @@ def fire_event(name, metadata):
True,
False,
existing_rates,
None,
dispatches_result,
True,
fire_event
Expand Down Expand Up @@ -191,6 +194,7 @@ def fire_event(name, metadata):
True,
False,
existing_rates,
None,
dispatches_result,
True,
fire_event
Expand Down Expand Up @@ -249,6 +253,7 @@ def fire_event(name, metadata):
True,
False,
existing_rates,
None,
dispatches_result,
True,
fire_event
Expand Down Expand Up @@ -318,6 +323,7 @@ def fire_event(name, metadata):
True,
False,
existing_rates,
None,
dispatches_result,
True,
fire_event
Expand Down Expand Up @@ -370,6 +376,7 @@ def fire_event(name, metadata):
True,
False,
existing_rates,
None,
dispatches_result,
True,
fire_event
Expand Down Expand Up @@ -439,6 +446,7 @@ def fire_event(name, metadata):
True,
is_export_meter,
existing_rates,
None,
dispatches_result,
True,
fire_event
Expand Down Expand Up @@ -525,6 +533,7 @@ def fire_event(name, metadata):
True,
False,
existing_rates,
None,
dispatches_result,
planned_dispatches_supported,
fire_event
Expand Down Expand Up @@ -589,6 +598,7 @@ def fire_event(name, metadata):
True,
False,
existing_rates,
None,
dispatches_result,
True,
fire_event
Expand Down Expand Up @@ -650,6 +660,7 @@ def fire_event(name, metadata):
True,
False,
existing_rates,
None,
dispatches_result,
True,
fire_event
Expand Down Expand Up @@ -734,6 +745,7 @@ def fire_event(name, metadata):
True,
is_export_meter,
existing_rates,
None,
dispatches_result,
planned_dispatches_supported,
fire_event
Expand Down Expand Up @@ -787,6 +799,7 @@ def fire_event(name, metadata):
True,
False,
existing_rates,
None,
dispatches_result,
True,
fire_event
Expand All @@ -797,7 +810,7 @@ def fire_event(name, metadata):
assert len(actual_fired_events.keys()) == 0

@pytest.mark.asyncio
async def test_when_rate_is_intelligent_and_dispatches_not_available_then_existing_rates_returned():
async def test_when_rate_is_intelligent_and_intelligent_device_is_available_and_dispatches_not_available_then_existing_rates_returned():
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])
Expand All @@ -816,6 +829,16 @@ def fire_event(name, metadata):
account_info = get_account_info(product_code="INTELLI-VAR-22-10-14")
existing_rates = ElectricityRatesCoordinatorResult(period_to - timedelta(days=60), 1, create_rate_data(period_from - timedelta(days=60), period_to - timedelta(days=60), [2, 4]))
dispatches_result = None
intelligent_device = IntelligentDevice(
"1",
"TESLA",
"Tesla",
"Model Y",
75.0,
"MyEnergi",
"Zappi",
6.5
)

with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates):
client = OctopusEnergyApiClient("NOT_REAL")
Expand All @@ -828,6 +851,7 @@ def fire_event(name, metadata):
True,
False,
existing_rates,
intelligent_device,
dispatches_result,
True,
fire_event
Expand All @@ -837,6 +861,88 @@ def fire_event(name, metadata):
assert mock_api_called == False
assert len(actual_fired_events.keys()) == 0

@pytest.mark.asyncio
async def test_when_rate_is_intelligent_and_intelligent_device_is_not_available_and_dispatches_not_available_then_rates_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)
mock_api_called = False
async def async_mocked_get_electricity_rates(*args, **kwargs):
nonlocal mock_api_called
mock_api_called = 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()
expected_original_rates = create_rate_data(expected_period_from, expected_period_to, [1, 2, 3, 4])
existing_rates = ElectricityRatesCoordinatorResult(current - timedelta(minutes=4, seconds=59), 1, expected_original_rates.copy())
expected_dispatch_start = (current + timedelta(hours=2)).replace(second=0, microsecond=0)
expected_dispatch_end = expected_dispatch_start + timedelta(minutes=90)
dispatches_result = IntelligentDispatchesCoordinatorResult(existing_rates.last_retrieved + timedelta(seconds=1), 1, IntelligentDispatches(
[
IntelligentDispatchItem(
expected_dispatch_start,
expected_dispatch_end,
1,
"smart-charge",
"home"
)
],
[]
))
intelligent_device = None

with mock.patch.multiple(OctopusEnergyApiClient, async_get_electricity_rates=async_mocked_get_electricity_rates):
client = OctopusEnergyApiClient("NOT_REAL")
retrieved_rates: ElectricityRatesCoordinatorResult = await async_refresh_electricity_rates_data(
current,
client,
account_info,
mpan,
serial_number,
True,
False,
existing_rates,
intelligent_device,
dispatches_result,
True,
fire_event
)

assert retrieved_rates is not None
assert retrieved_rates.last_retrieved == existing_rates.last_retrieved
assert retrieved_rates.original_rates == expected_original_rates
assert retrieved_rates.rates_last_adjusted == current

assert len(retrieved_rates.rates) == len(existing_rates.rates)

number_of_intelligent_rates = 0
expected_number_of_intelligent_rates = 3
for index in range(len(retrieved_rates.rates)):
expected_rate = existing_rates.rates[index]
actual_rate = retrieved_rates.rates[index]

if actual_rate["start"] >= expected_dispatch_start and actual_rate["end"] <= 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 mock_api_called == False
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
@pytest.mark.parametrize("current_unique_rates,previous_unique_rates,expected_unique_rates_changed_event_fired",[
([1, 2, 3], [1, 2, 3], False),
Expand Down Expand Up @@ -886,6 +992,7 @@ async def unique_rates_changed(name, metadata):
False,
existing_rates,
None,
None,
True,
fire_event,
unique_rates_changed=unique_rates_changed
Expand Down

0 comments on commit 50a7bdf

Please sign in to comment.