Skip to content

Opower: fall back to usage-only reads when cost endpoint fails#169224

Closed
loganrosen wants to merge 3 commits into
home-assistant:devfrom
loganrosen:opower-graceful-cost-fallback
Closed

Opower: fall back to usage-only reads when cost endpoint fails#169224
loganrosen wants to merge 3 commits into
home-assistant:devfrom
loganrosen:opower-graceful-cost-fallback

Conversation

@loganrosen
Copy link
Copy Markdown
Contributor

@loganrosen loganrosen commented Apr 27, 2026

Proposed change

When the Opower cost API returns an error (e.g., HTTP 500) for daily or hourly cost reads, retry with the usage-only endpoint before falling back to coarser data. This preserves consumption statistics at the original granularity (daily/hourly) rather than degrading to monthly.

Some utilities like ConEd return HTTP 500 from the REST cost endpoint for daily/hourly aggregation, while the usage-only endpoint works fine at those granularities. The account reports read_resolution: QUARTER_HOUR, and granular data does exist — it's specifically the cost endpoint that fails.

The fallback chain is now:

  1. Try cost endpoint (includes both consumption and cost)
  2. If that fails, try usage-only endpoint (consumption only, cost=0)
  3. If that also fails, return whatever coarser data was already fetched

Previously, any cost read failure at the daily or hourly level would crash the DataUpdateCoordinator. On retry, async_login() would be called again within the same TOTP window, causing the reused code to be rejected — creating a permanent reauth repair loop with no user-recoverable path.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Additional information

  • Affects any utility where the Opower cost API returns persistent errors at the daily/hourly level (ConEd, PSE, PG&E have all been reported)
  • The TOTP reuse on retry is tracked upstream at tronikos/opower#52; this PR prevents the coordinator crash that triggers it

Checklist

  • The code change is tested and works locally.
  • There is no commented out code in this PR.
  • Tests have been added to verify that the new code works.

Fixes #169223

When the Opower cost API returns an error (e.g., HTTP 500) for daily or
hourly cost reads, fall back to the coarser-resolution data that was
already fetched successfully, rather than re-raising the exception and
crashing the coordinator.

Previously, any cost read failure at the daily or hourly level would
propagate up and crash the DataUpdateCoordinator. On retry, the
coordinator would call async_login() again within the same 30-second
TOTP window, causing the reused code to be rejected. This created a
permanent reauth repair loop with no user-recoverable path.

Now, if daily reads fail, the coordinator returns the monthly (BILL)
data. If hourly reads fail, it returns the monthly+daily data. The
monthly (BILL) handler still raises on failure since there is no
coarser fallback. CostRead objects contain both consumption and cost,
so returning the coarser reads preserves consumption statistics at
reduced granularity rather than losing all data.

Fixes home-assistant#169223

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 27, 2026 00:13
@loganrosen loganrosen requested a review from tronikos as a code owner April 27, 2026 00:13
@home-assistant
Copy link
Copy Markdown
Contributor

Hey there @tronikos, mind taking a look at this pull request as it has been labeled with an integration (opower) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of opower can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant mark-draft Mark the pull request as draft.
  • @home-assistant ready-for-review Remove the draft status from the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign opower Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant update-branch Update the pull request branch with the base branch.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) on the pull request.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Opower integration’s coordinator to avoid crashing the DataUpdateCoordinator when finer-grained cost read endpoints (daily/hourly) error, falling back to coarser data that was already fetched successfully.

Changes:

  • In _async_get_cost_reads(), return already-fetched coarser cost reads when daily/hourly requests raise ApiException, instead of re-raising.
  • Adjust coordinator API-exception tests to no longer expect DAY/HOUR failures to raise.
  • Add a new test ensuring daily/hourly API errors don’t crash the coordinator (fallback behavior).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
homeassistant/components/opower/coordinator.py Implements fallback to coarser cost reads on DAY/HOUR API exceptions via early return + warning logs.
tests/components/opower/test_coordinator.py Updates exception expectations and adds a new test for fallback behavior on DAY/HOUR failures.

Comment thread homeassistant/components/opower/coordinator.py Outdated
Comment thread tests/components/opower/test_coordinator.py
- Fix hourly fallback log message to say 'using coarser reads' since
  cost_reads may be monthly-only if the daily call also failed
- Add async_wait_recording_done after _async_update_data in fallback
  test to avoid flaky pending recorder writes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Member

@tronikos tronikos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What read_resolution does the API return?
Weird that it advertises it supports granular reads but it only supports reads at the bill level.
Is that also the case in your utility website energy dashboard?
Does the GraphQL endpoint (sorry I haven't had time to review your PR) return granular data?

When the cost endpoint fails for daily/hourly reads, retry with
usage_only=True to hit the usage-only endpoint instead. This preserves
consumption data at the original granularity (daily/hourly) rather than
falling back to monthly data.

Some utilities like ConEd return HTTP 500 from the cost endpoint for
daily/hourly aggregation but the usage-only endpoint works fine. If
the usage-only endpoint also fails, fall back to the coarser data
that was already fetched.
Copilot AI review requested due to automatic review settings April 30, 2026 03:17
@loganrosen loganrosen changed the title Fall back to coarser cost reads on opower API errors Opower: fall back to usage-only reads when cost endpoint fails Apr 30, 2026
@loganrosen
Copy link
Copy Markdown
Contributor Author

From the diagnostics, the account reports read_resolution: "QUARTER_HOUR", so the API advertises full granularity. The failing URL is specifically the cost endpoint (/cws/cost/utilityAccount/...). I tested all the endpoints directly:

Endpoint BILL DAY HOUR
REST cost (/cws/cost/...) ✅ 26 reads ❌ 500 ❌ 500
REST usage-only (/cws/utilities/.../reads) ✅ 31 reads ✅ 180 reads
GraphQL (opower#177) ✅ 26 reads ✅ 31 reads ✅ 169 reads

The usage data exists at all granularities — it's specifically the REST cost endpoint that 500s for DAY/HOUR. Two things worth noting:

  1. The GraphQL interval reads return cost=0.0 for DAY/HOUR — ConEd doesn't provide sub-bill cost data. So cost is only meaningful at the BILL level for this utility.
  2. The REST usage-only endpoint works for DAY/HOUR. I've updated this PR to fall back to usage_only=True when the cost endpoint fails (instead of falling back to coarser BILL data). This preserves daily/hourly consumption granularity.

On the GraphQL side, I pushed a fix to opower#177 for a ConEd-specific issue: the timeInterval parameter needs UTC dates with Z suffix, not timezone offsets. With that fix, all three aggregation levels work for ConEd via GraphQL.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment on lines +574 to +575
except ApiException:
_LOGGER.warning("Usage-only daily reads also failed, using monthly")
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log the usage-only daily ApiException details (e.g., include the exception in the warning) so operators can see why both the primary and fallback calls failed.

Suggested change
except ApiException:
_LOGGER.warning("Usage-only daily reads also failed, using monthly")
except ApiException as err:
_LOGGER.warning(
"Usage-only daily reads also failed, using monthly: %s", err
)

Copilot uses AI. Check for mistakes.
Comment on lines +601 to +603
except ApiException:
_LOGGER.warning(
"Usage-only hourly reads also failed, using coarser reads"
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log the usage-only hourly ApiException details (e.g., include the exception in the warning) so failures don’t become silent when both the primary and fallback calls error.

Suggested change
except ApiException:
_LOGGER.warning(
"Usage-only hourly reads also failed, using coarser reads"
except ApiException as err:
_LOGGER.warning(
"Usage-only hourly reads also failed, using coarser reads: %s",
err,

Copilot uses AI. Check for mistakes.
Comment on lines +337 to +338


Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add assertions that the fallback path was actually exercised (e.g., that async_get_cost_reads was awaited with usage_only=True for the failing aggregate type) so this test fails if the coordinator regresses back to raising or stops invoking the fallback.

Suggested change
await_calls = mock_opower_api.async_get_cost_reads.await_args_list
assert any(
call.args[1] == failing_aggregate_type
and not call.kwargs.get("usage_only", False)
for call in await_calls
)
assert any(
call.args[1] == failing_aggregate_type
and call.kwargs.get("usage_only") is True
for call in await_calls
)

Copilot uses AI. Check for mistakes.
@tronikos
Copy link
Copy Markdown
Member

Change the library instead. There is already related code here:
https://github.com/tronikos/opower/blob/55162a8b881a4db9c1bc60c3a1b1012deb8963a9/src/opower/opower.py#L426-L428
It seems they started returning a non OK http status code instead of, or in addition to, returning empty data. What's the exact response you get from the API?

@tronikos tronikos closed this Apr 30, 2026
@loganrosen
Copy link
Copy Markdown
Contributor Author

loganrosen commented May 1, 2026

Makes sense. The exact response from the cost endpoint is:

HTTP Error: 500
URL: https://cned.opower.com/.../cws/cost/utilityAccount/<UUID>?aggregateType=day&startDate=2026-02-24T00:00:00-05:00&endDate=2026-04-27T00:00:00-04:00
Status: 500
Response: {"error":{"httpStatus":500,"serviceErrorCode":"UPSTREAM_ERROR","details":"Could not get rated costs and usages for utility account UUID: <UUID>"}}

So it's throwing an ApiException instead of returning empty data, which means the existing fallback on line 428 never triggers. I'll open a PR on the opower library to catch ApiException there and fall back to usage-only the same way.

@loganrosen loganrosen deleted the opower-graceful-cost-fallback branch May 1, 2026 02:17
@github-actions github-actions Bot locked and limited conversation to collaborators May 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Opower: daily/hourly cost read API errors crash coordinator, causing reauth loop

3 participants