Skip to content

feat: expose_recommended_entities service + voice/LLM docs (#65)#66

Merged
dewet22 merged 3 commits into
mainfrom
feat/expose-recommended-entities
May 28, 2026
Merged

feat: expose_recommended_entities service + voice/LLM docs (#65)#66
dewet22 merged 3 commits into
mainfrom
feat/expose-recommended-entities

Conversation

@dewet22

@dewet22 dewet22 commented May 28, 2026

Copy link
Copy Markdown
Owner

Closes #65.

Summary

Adds a one-shot service that exposes an opinionated headline set of 17 entities (battery SOC, PV/grid/load power, today's and lifetime energy totals, inverter status) to one or more voice/LLM assistants. Mirrors the `generate_dashboard` UX: run it once per inverter, get a persistent notification listing what was exposed, re-run any time without losing manual customisations.

Plus a new README section explaining how exposure works for this integration's headline entities.

Why a service (not an options-flow toggle)

The original issue body proposed an options-flow toggle for one-time auto-expose. After discussion we landed on a service instead, mirroring `generate_dashboard`:

  • Idempotent — re-run any time; the service only ever exposes, never un-exposes
  • No state to remember/reset — single transaction, no "did I already do this?" branching at setup
  • Doesn't fight the user — if they un-expose something later, the service won't re-add it unless you explicitly run it
  • Discoverable — Developer Tools → Services, scriptable from automations
  • Pattern users already know — same shape as the dashboard generator

The curated set (17 entities)

Topology variation is handled implicitly: PV-only installs skip battery entries, three-phase inverters use the same keys, etc. Matching is by `_{key}` suffix on `unique_id` — keys with no corresponding entity are silently skipped.

Question voice users ask Entities
"What's my battery at?" / "Is it charging?" `battery_soc`, `p_battery`
"How much did I charge/discharge today?" `e_battery_charge_day`, `e_battery_discharge_day`, `e_battery_throughput`
"Am I generating?" / "Solar today?" / "Lifetime PV?" `p_pv`, `e_pv_day`, `e_pv_total`
"Importing or exporting?" / "Today and lifetime grid?" `p_grid_out`, `e_grid_in_day`, `e_grid_out_day`, `e_grid_in_total`, `e_grid_out_total`
"What's the house using?" `p_load_demand`, `e_load_day`
"Lifetime inverter output?" `e_inverter_out_total`
"Is everything ok?" `inverter_status`

What's not included (and why)

  • Per-pack battery SOC — the inverter-level `battery_soc` answers "what's my battery at?" Multi-pack users can expose per-pack manually.
  • Per-MPPT detail (`p_pv1`, `v_pv1`, …) — diagnostic-grade, not voice-grade
  • Settings/controls (`charge_target_soc`, switches) — different threat model; exposing these lets the LLM change them
  • Voltage/current/frequency (`v_ac1`, `f_ac1`, …) — not what voice users ask about
  • `e_load_total` — doesn't exist at the inverter; HA's Energy dashboard derives lifetime load itself

Aliases — documented, not shipped

After a semble investigation of how HA Core integrations handle entity aliases, the verdict is don't seed aliases programmatically:

  1. Zero HA Core precedent — 107 calls to `async_update_entity` across all built-in integrations; none pass `aliases=`
  2. `async_get_or_create` deliberately omits aliases from its signature — every other "set at creation" field is there. Aliases are user-state by design.
  3. No "set by integration" marker — unlike `RegistryEntryHider` which has `INTEGRATION`/`USER` provenance, aliases carry none. Once we write them we can't tell ours from the user's; any re-seeding logic destroys user edits.
  4. First-position semantics in Google Assistant — position 0 of the alias list becomes the display name there. We'd have to prepend `COMPUTED_NAME` to stay safe.
  5. Equal-weight matching is a footgun for multi-inverter installs — two entities aliased `"battery"` → arbitrary intent-matcher winner.

The README documents a suggested starting set users can copy in 30 seconds.

Files

File Δ
`init.py` +80 — service handler, schema, registration
`const.py` +31 — `SERVICE_EXPOSE_RECOMMENDED_ENTITIES` + `EXPOSE_RECOMMENDED_ENTITY_KEYS` (curated list with comments)
`services.yaml` +34 — service stanza with device + assistants selectors
`translations/en.json` +14 — strings
`README.md` +51 — "Voice assistants & LLM access" section + services-table updates (also documents `redetect_plant` from #62 which was missing)
`tests/test_init.py` +71 — two happy-path tests (default assistant, custom assistants list)

Tests

Two new tests, both happy-path under HA's harness:

  1. Default `conversation` target — service walks the entry's entities, matches the curated key suffixes against `unique_id`, calls `async_expose_entity` for each. Asserts the right entity_ids were exposed, all to `"conversation"`.
  2. Custom `assistants` list — same flow with `assistants=["conversation", "cloud.alexa"]`. Asserts both platforms got every entity.

No error-branch tests (unknown device, no matching entry) — those exercise HA's device-registry lookup, not our code, matching the project's lightweight-test preference.

96 tests pass total (was 94 baseline).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • One-shot service to expose a recommended set of headline entities (battery SOC, PV/grid/load power, energy totals, inverter status) to voice/LLM assistants; requires a target device and lets you choose assistants (defaults to Conversation).
  • Documentation

    • Added "Voice assistants & LLM access" section with setup options, recommended entity mappings to common voice questions, suggested aliases, and rationale for not auto-exposing sensors.
  • Tests

    • Integration tests verifying recommended entities are exposed and custom assistant selections are honored.

Review Change Stack

Adds a one-shot service that exposes an opinionated headline set of 17
entities (battery SOC, PV/grid/load power, today's and lifetime energy
totals, inverter status) to one or more voice/LLM assistants. Mirrors the
dashboard generator's UX: user runs it once per inverter, gets a notification
listing what was exposed, and can re-run any time without losing manual
customisations (service only ever exposes, never un-exposes).

Default assistant is `conversation`, which covers Assist, the LLM tools API,
and MCP-via-conversation. Optional `assistants` parameter lets users target
`cloud.alexa` / `cloud.google_assistant` / custom assistants.

Topology variation is handled implicitly: PV-only installs skip battery
entries, three-phase inverters use the same entity keys, etc. — the service
matches by `_{key}` suffix on `unique_id` and silently skips keys with no
corresponding entity.

Also adds a "Voice assistants & LLM access" README section covering:
- Why exposure matters (HA's device-class allowlist excludes power/energy/
  battery, so headline sensors are invisible to voice/LLM by default)
- How to use the new service
- The recommended manual-expose set for users who prefer point-and-click
- Suggested aliases (documented, not shipped — entity-registry aliases have
  no provenance marker so we can't distinguish ours from user edits)
- Why HA filters these device classes out by default

While in the README services table, also documents the redetect_plant
service from #62 which wasn't added to README at the time.

Tests: two happy-path tests under HA's harness — one for the default
assistant target, one for a custom `assistants` list. Mirrors the lightweight
test pattern the project favours; no error-branch tests since those
exercise HA's device-registry lookup rather than our code.

Closes #65

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new service, expose_recommended_entities, which allows users to easily expose a curated, opinionated set of headline entities (such as battery SOC, PV power, and grid import/export) to voice and LLM assistants. It also includes comprehensive documentation in the README and corresponding unit tests. The review feedback focuses on improving robustness and usability: validating that the assistants list is not empty, defensively skipping disabled entities during exposure (and updating the tests accordingly), and formatting the list of exposed entities as a markdown bulleted list in the persistent notification for better readability.

Comment thread custom_components/givenergy_local/__init__.py
Comment thread custom_components/givenergy_local/__init__.py
Comment thread custom_components/givenergy_local/__init__.py Outdated
Comment thread tests/test_init.py Outdated
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

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: 7c862e7f-4360-4026-94ce-eab935698d61

📥 Commits

Reviewing files that changed from the base of the PR and between f725e44 and dc2afb8.

📒 Files selected for processing (2)
  • custom_components/givenergy_local/__init__.py
  • tests/test_init.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/test_init.py
  • custom_components/givenergy_local/init.py

📝 Walkthrough

Walkthrough

This PR implements a Home Assistant service to expose a recommended set of energy entities (battery state, power flows, energy totals, status) to voice assistants. Users call the service with a device ID and optional assistant list; the handler resolves the device, matches entities by configured key suffixes, exposes them via Home Assistant's exposure API, and notifies the user. Includes documentation, configuration metadata, and integration tests.

Changes

Voice Assistant Entity Exposure

Layer / File(s) Summary
Service definition and entity key configuration
custom_components/givenergy_local/const.py, custom_components/givenergy_local/services.yaml, custom_components/givenergy_local/translations/en.json
EXPOSE_RECOMMENDED_ENTITY_KEYS tuple defines recommended entities by unique_id suffix. Adds SERVICE_EXPOSE_RECOMMENDED_ENTITIES and service metadata in services.yaml plus English translations for the service fields.
Service handler implementation and lifecycle wiring
custom_components/givenergy_local/__init__.py
Adds imports for exposure APIs, SERVICE_EXPOSE_RECOMMENDED_ENTITIES_SCHEMA (required device_id, optional assistants defaulting to ["conversation"]), implements handle_expose_recommended_entities to map device → config entry, query the entity registry for matching suffixes, call async_expose_entity per assistant, raise if none matched, create a persistent notification, and register/unregister the service during setup/unload.
User-facing documentation and service discovery
README.md
Adds service table entry for expose_recommended_entities and a new "Voice assistants & LLM access" section describing manual vs service-driven exposure, lists the recommended entity set mapped to common voice questions, suggests aliases, and explains why the sensors are not auto-exposed.
Service behavior tests
tests/test_init.py
Adds test imports and two integration tests: one verifying default "conversation" assistant exposure for matched entities, another verifying a custom assistants list results in exposure calls to each requested assistant.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through code with a curious nose,
Taught battery and solar how to chat and pose.
Now Assist can ask and the sensors reply,
No more silent watts that pass us by.
Hooray — voices hear energy’s gentle throes!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: a new service for exposing recommended entities and voice/LLM documentation.
Linked Issues check ✅ Passed The PR fully addresses the requirements from issue #65: README section added with guidance and recommended entity set [#65]; opt-in programmatic exposure service implemented [#65]; aliases documented but not auto-seeded as intended [#65].
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #65 objectives: service implementation, documentation, constants, tests, and translations are all in-scope.

✏️ 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/expose-recommended-entities

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

Comment thread custom_components/givenergy_local/const.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/test_init.py (1)

300-323: ⚡ Quick win

Strengthen the custom-assistants test to assert full fan-out coverage.

This test currently checks only the assistant set; it can miss regressions where only a subset of entities is exposed.

Proposed enhancement
 async def test_expose_recommended_entities_honours_custom_assistants_list(
     hass, mock_client, setup_integration
 ):
@@
+    entity_reg = er.async_get(hass)
+    expected_entity_ids = {
+        entry.entity_id
+        for entry in er.async_entries_for_config_entry(entity_reg, setup_integration.entry_id)
+        for key in EXPOSE_RECOMMENDED_ENTITY_KEYS
+        if entry.unique_id.endswith(f"_{key}")
+    }
+
     with patch("custom_components.givenergy_local.async_expose_entity") as expose_mock:
@@
-    # Two assistants × N entities → 2N total exposure calls.
+    # Two assistants x N entities -> 2N total exposure calls.
     assistants_called = {call.args[1] for call in expose_mock.call_args_list}
     assert assistants_called == {"conversation", "cloud.alexa"}
+    assert len(expose_mock.call_args_list) == 2 * len(expected_entity_ids)
+    exposed_pairs = {(call.args[1], call.args[2]) for call in expose_mock.call_args_list}
+    assert exposed_pairs == {
+        (assistant, entity_id)
+        for assistant in {"conversation", "cloud.alexa"}
+        for entity_id in expected_entity_ids
+    }
🤖 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_init.py` around lines 300 - 323, Test
test_expose_recommended_entities_honours_custom_assistants_list currently only
asserts assistants were used and can miss missing entity exposures; update the
assertion to verify full fan-out by checking that async_expose_entity (patched
as expose_mock) was called for every (entity_id, assistant) pair invoked by
SERVICE_EXPOSE_RECOMMENDED_ENTITIES — e.g., compute the set of entity IDs
discovered from the device (or count N entities) and assert
len(expose_mock.call_args_list) == len(entities) * len(assistants) and/or assert
the set of (call.args[0], call.args[1]) pairs equals the Cartesian product of
entity IDs and the assistants list to ensure each entity was exposed for each
assistant.
🤖 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.

Inline comments:
In `@custom_components/givenergy_local/__init__.py`:
- Around line 110-114: The SERVICE_EXPOSE_RECOMMENDED_ENTITIES_SCHEMA currently
allows an empty "assistants" list; update that schema so "assistants" must be a
non-empty list. Modify SERVICE_EXPOSE_RECOMMENDED_ENTITIES_SCHEMA to validate
assistants with cv.ensure_list and a length check (e.g., vol.Length(min=1)) in
the vol.All chain and then validate items as strings so calls with assistants:
[] are rejected.

In `@tests/test_init.py`:
- Line 320: Replace the ambiguous Unicode multiplication sign in the comment
"Two assistants × N entities → 2N total exposure calls." by an ASCII character
(e.g., "x" or "*") so Ruff RUF003 is not triggered; update the comment to "Two
assistants x N entities → 2N total exposure calls." (or use "*") and then run
the project's formatter/linter fix (uv run ruff check --fix && uv run ruff
format) to apply and verify the change.

---

Nitpick comments:
In `@tests/test_init.py`:
- Around line 300-323: Test
test_expose_recommended_entities_honours_custom_assistants_list currently only
asserts assistants were used and can miss missing entity exposures; update the
assertion to verify full fan-out by checking that async_expose_entity (patched
as expose_mock) was called for every (entity_id, assistant) pair invoked by
SERVICE_EXPOSE_RECOMMENDED_ENTITIES — e.g., compute the set of entity IDs
discovered from the device (or count N entities) and assert
len(expose_mock.call_args_list) == len(entities) * len(assistants) and/or assert
the set of (call.args[0], call.args[1]) pairs equals the Cartesian product of
entity IDs and the assistants list to ensure each entity was exposed for each
assistant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f897a590-b440-42c8-9395-cb9cb096ff36

📥 Commits

Reviewing files that changed from the base of the PR and between d93767b and 2676133.

📒 Files selected for processing (6)
  • README.md
  • custom_components/givenergy_local/__init__.py
  • custom_components/givenergy_local/const.py
  • custom_components/givenergy_local/services.yaml
  • custom_components/givenergy_local/translations/en.json
  • tests/test_init.py

Comment thread custom_components/givenergy_local/__init__.py
Comment thread tests/test_init.py
blocking=True,
)

# Two assistants × N entities → 2N total exposure calls.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace ambiguous multiplication sign in comment.

Line 320 uses ×, which Ruff flags as RUF003.

Proposed fix
-    # Two assistants × N entities → 2N total exposure calls.
+    # Two assistants x N entities -> 2N total exposure calls.

As per coding guidelines **/*.py: Run uv run ruff check --fix && uv run ruff format before committing code.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Two assistants × N entities 2N total exposure calls.
# Two assistants x N entities -> 2N total exposure calls.
🧰 Tools
🪛 Ruff (0.15.14)

[warning] 320-320: Comment contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF003)

🤖 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_init.py` at line 320, Replace the ambiguous Unicode multiplication
sign in the comment "Two assistants × N entities → 2N total exposure calls." by
an ASCII character (e.g., "x" or "*") so Ruff RUF003 is not triggered; update
the comment to "Two assistants x N entities → 2N total exposure calls." (or use
"*") and then run the project's formatter/linter fix (uv run ruff check --fix &&
uv run ruff format) to apply and verify the change.

dewet22 and others added 2 commits May 28, 2026 03:09
The inverter status sensor is defined with `key="status"` and
`translation_key="inverter_status"`. The curated set had `inverter_status`,
which never matched `unique_id.endswith("_inverter_status")` because the
unique_id suffix is `_status` — so the documented "is everything ok?" entity
was silently skipped on every service call.

The existing test missed this because both its expected-set computation
and the production matching logic used the same broken comparison —
classic tautology. Rework the test to look entities up by their literal
unique_id (`{serial}_{key}`), which fails loudly when a curated key isn't
the actual entity description key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four small improvements from the PR #66 review pass:

- Skip disabled entities in the handler — disabled entities have no
  registry state for assistants to consume, so exposing them is a no-op
  at best and a confusion at worst.
- Enforce assistants list non-empty via vol.Length(min=1) so an explicit
  empty list raises at validation time rather than silently producing
  an empty notification.
- Format the exposed-entity notification as a bulleted markdown list
  including entity_ids — 17 comma-separated names was hard to scan.
- Mirror the disabled-entity filter in the test so the all-or-nothing
  assertion accounts for it correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dewet22 dewet22 merged commit b5ff146 into main May 28, 2026
8 checks passed
@dewet22 dewet22 deleted the feat/expose-recommended-entities branch May 28, 2026 10:54
@dewet22 dewet22 mentioned this pull request May 28, 2026
3 tasks
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.

Document Assist/LLM exposure for energy entities; consider opt-in auto-expose

1 participant