Skip to content

feat: add bill history sensors and fix energy_cost alignment#26

Merged
virtitnerd merged 4 commits into
mainfrom
feat/bill-history-sensors
May 24, 2026
Merged

feat: add bill history sensors and fix energy_cost alignment#26
virtitnerd merged 4 commits into
mainfrom
feat/bill-history-sensors

Conversation

@virtitnerd
Copy link
Copy Markdown
Owner

@virtitnerd virtitnerd commented May 24, 2026

What

Adds bill history sensors powered by the National Grid business portal API (get_electric_bill_history / get_gas_bill_history from py-nationalgrid 0.6.3). These return per-billing-period breakdowns not available from the existing GraphQL billing calls.

Also fixes the existing Last Billing Cost (energy_cost) sensor: it previously sourced from get_energy_usage_costs (consumer portal monthly estimates) which does not correspond to actual billing periods. Replacing it with totalCharges from bill history makes electric.totalCharges + gas.totalCharges = current_bill, matching what National Grid shows on the account page. Note: on solar-heavy months totalCharges can be less than utilityCharges alone because National Grid applies credits after the utility+supplier calculation.

New sensors (meter-level, one set per meter)

Key Description
last_bill_utility_charges Utility portion of last bill (USD)
last_bill_supplier_charges Supplier portion of last bill (USD)
last_bill_avg_daily_usage Average daily usage for last bill period (kWh / CCF)

Coordinator changes

  • electric_bill_history / gas_bill_history fields added to NationalGridCoordinatorData
  • _fetch_bill_history() called after _fetch_bills() in _fetch_account_data()
  • customerNumber coerced to str (model returns int, endpoint expects a string)
  • fuelTypes entries are dicts {"type": "ELECTRIC"} — extracted with ft.get("type"), not cast to string directly

Test plan

  • pytest passes (221 tests, 100% coverage)
  • ruff check / ruff format --check clean
  • Electric + gas last_billing_cost sensors sum equals current_bill in live HA

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Per-account electric and gas bill history is now cached and used to surface recent billing data.
    • Three new sensors show last bill utility charges, supplier charges, and average daily usage.
    • Energy cost reporting now uses the most recent bill totals for more accurate values.
    • English labels added for the new sensors.

Review Change Stack

virtitnerd and others added 3 commits May 23, 2026 21:24
Adds _mock_electric_bill_history() and _mock_gas_bill_history()
helpers, and updates _mock_billing_account() with customerNumber
and fuelTypes fields required by the bill history fetch path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds electric_bill_history and gas_bill_history fields to
NationalGridCoordinatorData, a _fetch_bill_history() method that
calls get_electric_bill_history/get_gas_bill_history from the
business portal SSO, and helper methods
get_latest_electric_bill_record/get_latest_gas_bill_record.

customerNumber is coerced to str before the API call (the model
returns int but the endpoint expects a string in the request body).
fuelTypes entries are dicts like {"type": "ELECTRIC"}, extracted
with ft.get("type") — not flat strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds three new meter-level sensors driven by the business-portal
bill history API:
  - last_bill_utility_charges
  - last_bill_supplier_charges
  - last_bill_avg_daily_usage (kWh for electric, CCF for gas)

Also fixes the existing energy_cost sensor: it previously used
get_energy_usage_costs (consumer portal monthly estimates) which
does not match billing cycles. Replacing its value_fn with
_get_total_charges (from bill history) makes electric + gas
totalCharges sum equal the current_bill sensor, matching what
National Grid shows on the account page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 24, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6768aff6-39aa-42a8-9082-a213f9869ee4

📥 Commits

Reviewing files that changed from the base of the PR and between 9bfa9c4 and ce48a5b.

📒 Files selected for processing (2)
  • tests/test_coordinator.py
  • tests/test_sensor.py

📝 Walkthrough

Walkthrough

Bill history is fetched per account from the business-portal API during account refresh, cached by account and fuel type in the coordinator, and exposed via getters used by new sensor helpers. Sensors derive last-bill utility charges, supplier charges, average daily usage, and energy-cost now reads total charges from bill history.

Changes

Bill History Feature

Layer / File(s) Summary
Coordinator data structure and types
custom_components/national_grid/coordinator.py
Types ElectricBillRecord and GasBillRecord are imported. NationalGridCoordinatorData gains electric_bill_history and gas_bill_history dictionaries keyed by account ID and seeded from prior coordinator state.
Bill history fetching and access
custom_components/national_grid/coordinator.py
_fetch_account_data() calls new _fetch_bill_history() after account fetch. The helper checks customerNumber and fuelTypes, conditionally calls electric/gas business-portal history APIs, caches results, logs warnings on exceptions without raising, and public getters return the latest record per fuel type.
Sensor bill history helpers and integration
custom_components/national_grid/sensor.py
New helpers _get_bill_history_record(), _get_utility_charges(), _get_supplier_charges(), _get_avg_daily_usage(), and _get_total_charges() derive metrics from the latest bill record. Removed _get_energy_cost(). energy_cost now uses _get_total_charges().
New sensor entity definitions
custom_components/national_grid/sensor.py
Added sensors last_bill_utility_charges, last_bill_supplier_charges, and last_bill_avg_daily_usage to SENSOR_DESCRIPTIONS, each wired to helpers and configured with monetary/energy units and always-available availability.
User-facing strings and translations
custom_components/national_grid/strings.json, custom_components/national_grid/translations/en.json
Added labels and English translations for cost_per_unit and last-bill metrics.
Test fixtures and mock setup
requirements_test.txt, tests/conftest.py
Pinned py-nationalgrid==0.6.3. Mock billing account gains customerNumber and fuelTypes. New mock helpers _mock_electric_bill_history() and _mock_gas_bill_history() provide fixture bill records.
Coordinator bill history tests
tests/test_coordinator.py
Tests verify fetching and caching per fuel type, non-raising failure logging, latest-record getters (including empty/None cases), skip conditions (no customerNumber, interval-only, account not loaded), and preservation of prior history across failing incremental refreshes.
Sensor helper tests
tests/test_sensor.py
Tests validate bill-record selection by fuel type, extraction of utility/supplier charges, avg daily usage, total charges, correct unit mapping for avg usage, and presence of new sensor descriptions; energy-cost tests now assert total-charges behavior.

Sequence Diagram

sequenceDiagram
  participant DataUpdateCoordinator
  participant BusinessPortalAPI
  participant CoordinatorData
  participant SensorHelper
  DataUpdateCoordinator->>BusinessPortalAPI: _fetch_bill_history(account_id, customerNumber, fuelTypes)
  BusinessPortalAPI-->>DataUpdateCoordinator: electric_bill_records / gas_bill_records
  DataUpdateCoordinator->>CoordinatorData: store in electric_bill_history[account_id] / gas_bill_history[account_id]
  SensorHelper->>DataUpdateCoordinator: get_latest_electric_bill_record(account_id)
  DataUpdateCoordinator-->>SensorHelper: latest record or None
  SensorHelper->>SensorHelper: _get_total_charges(record)
  SensorHelper-->>SensorHelper: return totalCharges or None
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit found the billing thread,

Cached records hopping in its head,
Utility, supplier, daily use—so neat,
New sensors show the last-bill sheet,
Hooray for data tidy and sweet!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: adding bill history sensors and fixing energy_cost alignment, directly matching the changeset's primary objectives.
Docstring Coverage ✅ Passed Docstring coverage is 96.77% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bill-history-sensors

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 24, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/test_coordinator.py (1)

1095-1119: ⚡ Quick win

Assert customerNumber is passed as str to bill-history API calls.

These tests validate stored results, but they don’t currently lock down the coercion contract. A regression could pass an int and still pass with permissive mocks.

Proposed test hardening
 async def test_fetch_bill_history_electric(hass: HomeAssistant) -> None:
@@
     records = coordinator.data.electric_bill_history.get(MOCK_ACCOUNT_ID, [])
     assert len(records) == 2
     assert records[0]["utilityCharges"] == 98.40
     assert records[0]["supplierCharges"] == 47.10
     assert records[0]["avgDailyUsage"] == 16.77
+    call = api.get_electric_bill_history.await_args
+    customer_number = call.kwargs.get("customer_number")
+    if customer_number is None and call.args:
+        customer_number = call.args[0]
+    assert isinstance(customer_number, str)

 async def test_fetch_bill_history_gas(hass: HomeAssistant) -> None:
@@
     records = coordinator.data.gas_bill_history.get(MOCK_ACCOUNT_ID, [])
     assert len(records) == 1
     assert records[0]["utilityCharges"] == 28.80
     assert records[0]["avgDailyUsage"] == 1.03
+    call = api.get_gas_bill_history.await_args
+    customer_number = call.kwargs.get("customer_number")
+    if customer_number is None and call.args:
+        customer_number = call.args[0]
+    assert isinstance(customer_number, str)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_coordinator.py` around lines 1095 - 1119, The tests
test_fetch_bill_history_electric and test_fetch_bill_history_gas currently
validate stored records but don’t assert the type of customerNumber passed to
the bill-history API; update each test (using the existing _make_api mock and
MOCK_ACCOUNT_ID) to also assert that the API’s bill-history call was invoked
with customerNumber equal to str(MOCK_ACCOUNT_ID) (i.e., a string), for example
by inspecting the mock call args after coordinator._async_update_data()
completes so the contract enforces coercion to str before calling the API.
tests/test_sensor.py (1)

448-463: ⚡ Quick win

Add gas-path coverage for _get_supplier_charges.

A gas-specific assertion is missing here; a gas-branch regression could slip through while current tests still pass.

Proposed additional test
 def test_get_supplier_charges_electric() -> None:
@@
     coordinator.get_latest_electric_bill_record.return_value = electric_rec
     assert _get_supplier_charges(coordinator, meter_data) == 47.10

+def test_get_supplier_charges_gas() -> None:
+    """Test _get_supplier_charges returns supplierCharges from gas record."""
+    meter_data = _make_meter_data("Gas")
+    coordinator = MagicMock()
+    coordinator.get_latest_gas_bill_record.return_value = _make_gas_bill_record()
+    assert _get_supplier_charges(coordinator, meter_data) == 16.20
+
 def test_get_supplier_charges_none_when_no_record() -> None:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_sensor.py` around lines 448 - 463, Add a gas-path unit test
mirroring the electric tests: create a meter_data via _make_meter_data("Gas"),
stub coordinator.get_latest_gas_bill_record to return a gas bill record (use or
add _make_gas_bill_record if needed), call _get_supplier_charges(coordinator,
meter_data) and assert it equals the expected supplierCharges value; also add a
companion test where coordinator.get_latest_gas_bill_record returns None and
assert _get_supplier_charges returns None. Ensure tests are named e.g.
test_get_supplier_charges_gas and test_get_supplier_charges_gas_none and
reference _get_supplier_charges, _make_meter_data,
coordinator.get_latest_gas_bill_record, and _make_gas_bill_record so the gas
branch is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_coordinator.py`:
- Around line 1095-1119: The tests test_fetch_bill_history_electric and
test_fetch_bill_history_gas currently validate stored records but don’t assert
the type of customerNumber passed to the bill-history API; update each test
(using the existing _make_api mock and MOCK_ACCOUNT_ID) to also assert that the
API’s bill-history call was invoked with customerNumber equal to
str(MOCK_ACCOUNT_ID) (i.e., a string), for example by inspecting the mock call
args after coordinator._async_update_data() completes so the contract enforces
coercion to str before calling the API.

In `@tests/test_sensor.py`:
- Around line 448-463: Add a gas-path unit test mirroring the electric tests:
create a meter_data via _make_meter_data("Gas"), stub
coordinator.get_latest_gas_bill_record to return a gas bill record (use or add
_make_gas_bill_record if needed), call _get_supplier_charges(coordinator,
meter_data) and assert it equals the expected supplierCharges value; also add a
companion test where coordinator.get_latest_gas_bill_record returns None and
assert _get_supplier_charges returns None. Ensure tests are named e.g.
test_get_supplier_charges_gas and test_get_supplier_charges_gas_none and
reference _get_supplier_charges, _make_meter_data,
coordinator.get_latest_gas_bill_record, and _make_gas_bill_record so the gas
branch is covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d65a34eb-233c-4a4d-93f4-4ca0a23b3d2c

📥 Commits

Reviewing files that changed from the base of the PR and between 8359c3a and 9bfa9c4.

📒 Files selected for processing (8)
  • custom_components/national_grid/coordinator.py
  • custom_components/national_grid/sensor.py
  • custom_components/national_grid/strings.json
  • custom_components/national_grid/translations/en.json
  • requirements_test.txt
  • tests/conftest.py
  • tests/test_coordinator.py
  • tests/test_sensor.py

…s tests

In test_coordinator.py: assert get_electric_bill_history and
get_gas_bill_history are called with customerNumber as a string
("987654321") so the int→str coercion in _fetch_bill_history is
a hard contract, not just an implementation detail.

In test_sensor.py: add test_get_supplier_charges_gas and
test_get_supplier_charges_gas_none to cover the gas branch of
_get_supplier_charges, mirroring the existing electric tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@virtitnerd virtitnerd merged commit a4ded15 into main May 24, 2026
8 checks passed
@virtitnerd virtitnerd deleted the feat/bill-history-sensors branch May 24, 2026 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant