Skip to content

Commit 44d2db2

Browse files
committed
feat: Updated started dispatch calculation to be more forgiving on how stale the data is. A planned dispatch will only transition to a started dispatch if data has been retrieved within the last 3 minutes (1 hour dev time)
1 parent 58943e9 commit 44d2db2

File tree

3 files changed

+108
-16
lines changed

3 files changed

+108
-16
lines changed

_docs/entities/intelligent.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ This sensor is used to determine if you're currently in a planned dispatch perio
3030
|-----------|------|-------------|
3131
| `planned_dispatches` | `array` | An array of the dispatches that are currently planned by Octopus Energy. |
3232
| `completed_dispatches` | `array` | An array of the dispatches that have been completed by Octopus Energy. This will only store up to the last 3 days worth of completed dispatches. |
33-
| `started_dispatches` | `array` | An array of the dispatches that have been planned by Octopus Energy and upon API refresh are still planned when the current 30 minute period started and is not in a boosting state. A planned dispatch will be added one 30 minute period at a time. This will only store up to the last 3 days worth of started dispatches. This is used to determine historic off peak rates. For example if you have a planned dispatch of `2025-04-01T10:00:00`-`2025-04-01T11:00:00`, at `2025-04-01T10:01:00` if the planned dispatch is still available the period of `2025-04-01T10:00:00`-`2025-04-01T10:30:00` will be added. |
33+
| `started_dispatches` | `array` | An array of the dispatches that have been planned by Octopus Energy and upon API refresh are still planned when the current 30 minute period has started, is not in a boosting state and the data has been refreshed within the last 3 minutes. A planned dispatch will be added one 30 minute period at a time. This will only store up to the last 3 days worth of started dispatches. This is used to determine historic off peak rates. For example if you have a planned dispatch of `2025-04-01T10:00:00`-`2025-04-01T11:00:00`, at `2025-04-01T10:01:00` if the planned dispatch is still available the period of `2025-04-01T10:00:00`-`2025-04-01T10:30:00` will be added. |
3434
| `provider` | `string` | The provider of the intelligent features |
3535
| `vehicle_battery_size_in_kwh` | `float` | The size of the target vehicle battery in kWh. |
3636
| `charge_point_power_in_kw` | `float` | The power of the charge point battery in kW. |

custom_components/octopus_energy/coordinators/intelligent_dispatches.py

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,10 @@ def has_dispatches_changed(existing_dispatches: IntelligentDispatches, new_dispa
109109
)
110110
)
111111

112-
def merge_started_dispatches(current: datetime, current_state: str, started_dispatches: list[SimpleIntelligentDispatchItem], planned_dispatches: list[IntelligentDispatchItem]):
112+
def merge_started_dispatches(current: datetime,
113+
current_state: str,
114+
started_dispatches: list[SimpleIntelligentDispatchItem],
115+
planned_dispatches: list[IntelligentDispatchItem]):
113116
new_started_dispatches = clean_previous_dispatches(current, started_dispatches)
114117

115118
if current_state == "SMART_CONTROL_IN_PROGRESS":
@@ -137,7 +140,7 @@ def merge_started_dispatches(current: datetime, current_state: str, started_disp
137140

138141
return new_started_dispatches
139142

140-
async def async_refresh_intelligent_dispatches(
143+
async def async_retrieve_intelligent_dispatches(
141144
current: datetime,
142145
client: OctopusEnergyApiClient,
143146
account_info,
@@ -146,7 +149,6 @@ async def async_refresh_intelligent_dispatches(
146149
is_data_mocked: bool,
147150
is_manual_refresh: bool,
148151
planned_dispatches_supported: bool,
149-
async_save_dispatches: Callable[[str, IntelligentDispatches], Awaitable[list]],
150152
):
151153
requests_current_hour = existing_intelligent_dispatches_result.requests_current_hour if existing_intelligent_dispatches_result is not None else 0
152154
requests_last_reset = existing_intelligent_dispatches_result.requests_current_hour_last_reset if existing_intelligent_dispatches_result is not None else current
@@ -208,18 +210,8 @@ async def async_refresh_intelligent_dispatches(
208210
dispatches.planned.clear()
209211

210212
if dispatches is not None:
211-
dispatches.completed = clean_previous_dispatches(utcnow(), (existing_intelligent_dispatches_result.dispatches.completed if existing_intelligent_dispatches_result is not None and existing_intelligent_dispatches_result.dispatches is not None and existing_intelligent_dispatches_result.dispatches.completed is not None else []) + dispatches.completed)
212-
dispatches.started = merge_started_dispatches(current,
213-
dispatches.current_state,
214-
existing_intelligent_dispatches_result.dispatches.started
215-
if existing_intelligent_dispatches_result is not None and existing_intelligent_dispatches_result.dispatches is not None and existing_intelligent_dispatches_result.dispatches.started is not None
216-
else [],
217-
dispatches.planned)
218-
219-
if (existing_intelligent_dispatches_result is None or
220-
existing_intelligent_dispatches_result.dispatches is None or
221-
has_dispatches_changed(existing_intelligent_dispatches_result.dispatches, dispatches)):
222-
await async_save_dispatches(account_id, dispatches)
213+
dispatches.completed = clean_previous_dispatches(current,
214+
(existing_intelligent_dispatches_result.dispatches.completed if existing_intelligent_dispatches_result is not None and existing_intelligent_dispatches_result.dispatches is not None and existing_intelligent_dispatches_result.dispatches.completed is not None else []) + dispatches.completed)
223215

224216
return IntelligentDispatchesCoordinatorResult(current, 1, dispatches, requests_current_hour + 1, requests_last_reset)
225217

@@ -245,6 +237,48 @@ async def async_refresh_intelligent_dispatches(
245237

246238
return existing_intelligent_dispatches_result
247239

240+
async def async_refresh_intelligent_dispatches(
241+
current: datetime,
242+
client: OctopusEnergyApiClient,
243+
account_info,
244+
intelligent_device: IntelligentDevice,
245+
existing_intelligent_dispatches_result: IntelligentDispatchesCoordinatorResult,
246+
is_data_mocked: bool,
247+
is_manual_refresh: bool,
248+
planned_dispatches_supported: bool,
249+
async_save_dispatches: Callable[[str, IntelligentDispatches], Awaitable[list]],
250+
):
251+
result = await async_retrieve_intelligent_dispatches(
252+
current,
253+
client,
254+
account_info,
255+
intelligent_device,
256+
existing_intelligent_dispatches_result,
257+
is_data_mocked,
258+
is_manual_refresh,
259+
planned_dispatches_supported
260+
)
261+
262+
if result is not None and result.dispatches is not None:
263+
if result.last_retrieved < (current - timedelta(minutes=REFRESH_RATE_IN_MINUTES_INTELLIGENT)):
264+
_LOGGER.debug('Skipping started dispatches processing as data has not been refreshed recently')
265+
# If we haven't refreshed recently, then we can't accurately process started dispatches
266+
return result
267+
268+
result.dispatches.started = merge_started_dispatches(current,
269+
result.dispatches.current_state,
270+
existing_intelligent_dispatches_result.dispatches.started
271+
if existing_intelligent_dispatches_result is not None and existing_intelligent_dispatches_result.dispatches is not None and existing_intelligent_dispatches_result.dispatches.started is not None
272+
else [],
273+
result.dispatches.planned)
274+
275+
if (existing_intelligent_dispatches_result is None or
276+
existing_intelligent_dispatches_result.dispatches is None or
277+
has_dispatches_changed(existing_intelligent_dispatches_result.dispatches, result.dispatches)):
278+
await async_save_dispatches(account_info["id"], result.dispatches)
279+
280+
return result
281+
248282
async def async_setup_intelligent_dispatches_coordinator(hass, account_id: str, mock_intelligent_data: bool, manual_dispatch_refreshes: bool, planned_dispatches_supported: bool):
249283
async def async_update_intelligent_dispatches_data(is_manual_refresh = False):
250284
"""Fetch data from API endpoint."""

tests/unit/coordinators/test_async_refresh_intelligent_dispatches.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,64 @@ async def async_save_dispatches(*args, **kwargs):
283283
assert save_dispatches_account_id == None
284284
assert save_dispatches_dispatches == None
285285

286+
@pytest.mark.asyncio
287+
async def test_when_existing_dispatches_returned_and_planned_dispatch_started_and_refreshed_recently_then_started_dispatches_updated():
288+
current = datetime.strptime("2023-07-14T10:30:01+01:00", "%Y-%m-%dT%H:%M:%S%z")
289+
expected_dispatches = IntelligentDispatches("SMART_CONTROL_IN_PROGRESS", [], [])
290+
mock_api_called = False
291+
async def async_mock_get_intelligent_dispatches(*args, **kwargs):
292+
nonlocal mock_api_called
293+
mock_api_called = True
294+
return expected_dispatches
295+
296+
save_dispatches_called = False
297+
save_dispatches_account_id = None
298+
save_dispatches_dispatches = None
299+
async def async_save_dispatches(*args, **kwargs):
300+
nonlocal save_dispatches_called, save_dispatches_account_id, save_dispatches_dispatches
301+
save_dispatches_called = True
302+
account_id, dispatches = args
303+
save_dispatches_account_id = account_id
304+
save_dispatches_dispatches = dispatches
305+
306+
account_info = get_account_info()
307+
existing_dispatches = IntelligentDispatchesCoordinatorResult(current - timedelta(minutes=2), 1, mock_intelligent_dispatches(), 1, last_retrieved)
308+
existing_dispatches.dispatches.current_state = "SMART_CONTROL_IN_PROGRESS"
309+
existing_dispatches.dispatches.planned = [
310+
IntelligentDispatchItem(
311+
current - timedelta(minutes=1),
312+
current + timedelta(minutes=45),
313+
1.1,
314+
None,
315+
"home"
316+
)
317+
]
318+
319+
with mock.patch.multiple(OctopusEnergyApiClient, async_get_intelligent_dispatches=async_mock_get_intelligent_dispatches):
320+
client = OctopusEnergyApiClient("NOT_REAL")
321+
retrieved_dispatches: IntelligentDispatchesCoordinatorResult = await async_refresh_intelligent_dispatches(
322+
current,
323+
client,
324+
account_info,
325+
intelligent_device,
326+
existing_dispatches,
327+
False,
328+
False,
329+
True,
330+
async_save_dispatches
331+
)
332+
333+
assert mock_api_called == False
334+
assert retrieved_dispatches == existing_dispatches
335+
336+
assert len(retrieved_dispatches.dispatches.started) == 1
337+
assert retrieved_dispatches.dispatches.started[0].start == current.replace(second=0, microsecond=0)
338+
assert retrieved_dispatches.dispatches.started[0].end == current.replace(second=0, microsecond=0) + timedelta(minutes=30)
339+
340+
assert save_dispatches_called == False
341+
assert save_dispatches_account_id == None
342+
assert save_dispatches_dispatches == None
343+
286344
@pytest.mark.asyncio
287345
@pytest.mark.parametrize("existing_dispatches",[
288346
(None),

0 commit comments

Comments
 (0)