Skip to content

Commit fd89670

Browse files
CIclaude
andcommitted
fix(octopus): keep the last planned dispatches when Octopus returns them as null (#5492)
Octopus can answer the dispatch query with an error for the planned dispatches alone (KT-CT-4340, "Unable to fetch planned dispatches") and flexPlannedDispatches null beside otherwise good data. The query ignores errors, so the null was read as an empty list and the car's plan was dropped until the next poll. A null planned list is now treated as a failed fetch: the last known planned dispatches are kept, each dropped once its end has passed and the one in progress re-trimmed, as when the whole query fails. An empty list still means no dispatches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d6985b6 commit fd89670

3 files changed

Lines changed: 60 additions & 3 deletions

File tree

‎apps/predbat/octopus.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2601,8 +2601,11 @@ async def async_get_intelligent_devices(self, account_id, device_id):
26012601
break
26022602
if not found:
26032603
completed.append(dispatch)
2604-
else:
2605-
# The dispatch query failed (e.g. rate limited). Keep the last known planned dispatches,
2604+
if not dispatch_result or dispatch_result.get("flexPlannedDispatches") is None:
2605+
# The dispatch query failed (e.g. rate limited), or Octopus answered it without the planned
2606+
# dispatches: an error on that field (KT-CT-4340, "Unable to fetch planned dispatches") comes
2607+
# back as null beside the rest of the data, where no dispatches is an empty list (#5492).
2608+
# Keep the last known planned dispatches,
26062609
# as a failed settings query keeps its settings: publishing an empty list dropped the car's
26072610
# slots and dispatch rates until the next poll, and the change requested a replan without them.
26082611
# Planned dispatches are never pruned, so drop any that have ended since, and re-trim the one

‎apps/predbat/tests/test_octopus_intelligent_devices.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,60 @@ async def mock_query_dispatch_fail(query, context, ignore_errors=False, returns_
841841
else:
842842
print("PASS: A never-seen device gets no planned dispatches when the dispatch query fails")
843843

844+
# ------------------------------------------------------------------
845+
# Test 15: Octopus can answer the dispatch query but fail the planned dispatches alone - an error
846+
# (KT-CT-4340, "Unable to fetch planned dispatches") with flexPlannedDispatches null beside the rest
847+
# of the data. That is a failed fetch of the planned list, kept as in Test 14, and not a statement
848+
# that there are none: read as an empty list it dropped the car's plan until the next poll (#5492).
849+
# The completed dispatches in the same answer are still good. An empty list is Octopus saying there
850+
# are no planned dispatches, and still clears them.
851+
# ------------------------------------------------------------------
852+
print("\n*** Test 15: A dispatch answer with the planned dispatches null keeps the last known ones ***")
853+
completed_start = (ref_now - timedelta(minutes=180)).strftime(DATE_TIME_STR_FORMAT)
854+
completed_end = (ref_now - timedelta(minutes=150)).strftime(DATE_TIME_STR_FORMAT)
855+
dispatch_answer = {
856+
"devices": [{"id": "device-abc", "status": {"currentState": "SMART_CONTROL_IN_PROGRESS"}}],
857+
"flexPlannedDispatches": None,
858+
"completedDispatches": [{"start": completed_start, "end": completed_end, "delta": "-3.5", "meta": {"source": "smart-charge", "location": "AT_HOME"}}],
859+
}
860+
861+
async def mock_query_planned_null(query, context, ignore_errors=False, returns_data=True):
862+
if "get-intelligent-devices" in context:
863+
return device_data
864+
elif "get-intelligent-dispatches" in context:
865+
return dispatch_answer
866+
elif "get-intelligent-settings" in context:
867+
return {"devices": [{"id": "device-abc", "status": {"isSuspended": False}, "chargingPreferences": {}}]}
868+
return None
869+
870+
api = make_api()
871+
api.intelligent_devices = {"device-abc": {"device_id": "device-abc", "suspended": False, "completed_dispatches": [], "planned_dispatches": cached_planned}}
872+
api.async_graphql_query = AsyncMock(side_effect=mock_query_planned_null)
873+
result = await api.async_get_intelligent_devices("test-account", "device-abc")
874+
device_result = result.get("device-abc", {})
875+
if device_result.get("planned_dispatches") != known_planned:
876+
print(f"ERROR: Expected the last known planned dispatches kept when Octopus returns them as null, got {device_result.get('planned_dispatches')}")
877+
failed += 1
878+
elif cached_planned != cached_copy:
879+
print(f"ERROR: Reusing the cached planned dispatches must not modify them in place, got {cached_planned}")
880+
failed += 1
881+
elif [x.get("start") for x in device_result.get("completed_dispatches", [])] != [completed_start]:
882+
print(f"ERROR: Expected the completed dispatches in the same answer still taken, got {device_result.get('completed_dispatches')}")
883+
failed += 1
884+
else:
885+
print("PASS: Planned dispatches returned as null keep the last known ones - ended dropped, running re-trimmed, completed still taken")
886+
887+
dispatch_answer["flexPlannedDispatches"] = []
888+
api = make_api()
889+
api.intelligent_devices = {"device-abc": {"device_id": "device-abc", "suspended": False, "completed_dispatches": [], "planned_dispatches": cached_planned}}
890+
api.async_graphql_query = AsyncMock(side_effect=mock_query_planned_null)
891+
result = await api.async_get_intelligent_devices("test-account", "device-abc")
892+
if result.get("device-abc", {}).get("planned_dispatches") != []:
893+
print(f"ERROR: An empty planned list from Octopus means no dispatches, got {result.get('device-abc', {}).get('planned_dispatches')}")
894+
failed += 1
895+
else:
896+
print("PASS: An empty planned list from Octopus still clears the planned dispatches")
897+
844898
if failed == 0:
845899
print("\n**** All Octopus intelligent devices tests PASSED ****")
846900
else:

0 commit comments

Comments
 (0)