devices: support Home Assistant 2026.8 ownership model - #168
Conversation
Adopt single-config-entry device ownership, migrate legacy helper devices, and link helper entities directly to source devices. Expand lifecycle and migration coverage, refresh documentation, and add test and formatting checks to CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reviewer's GuideUpdates the Utility Meter Next Gen custom integration to comply with Home Assistant 2026.8’s single-owner device model by replacing implicit helper-based device links with direct entity-to-device associations, refactoring config-entry migration and config flow versions, tightening typing and formatting, and adding CI-backed tests using pytest-homeassistant-custom-component. Sequence diagram for config entry migration to single-owner device modelsequenceDiagram
participant HA as HomeAssistant
participant CE as ConfigEntries
participant UM as async_migrate_entry
participant DR as async_entity_id_to_device_id
participant HD as async_remove_helper_devices
HA->>CE: trigger migration for config_entry
CE->>UM: async_migrate_entry(config_entry)
UM->>UM: normalize options and version
alt minor_version < 2
UM->>DR: async_entity_id_to_device_id(hass, options[CONF_SOURCE_SENSOR])
DR-->>UM: source_device_id
UM->>HD: async_remove_helper_devices(hass, helper_config_entry_id, source_device_id, remove_all_devices=True)
HD-->>UM: helper devices removed
end
UM->>CE: async_update_entry(config_entry, options, version=8, minor_version=2)
CE-->>HA: migration complete
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 6 issues, and left some high level feedback:
- The lint/test GitHub workflow is configured to use Python
3.14.6, which doesn’t currently exist; consider switching to a supported version (e.g.,3.12.x) that matches Home Assistant’s supported runtime so CI can actually run. - In
async_migrate_entry, the call toasync_entity_id_to_device_id(hass, options[CONF_SOURCE_SENSOR])assumes theCONF_SOURCE_SENSORoption is always present and non-null; you may want to guard this with agetor early return to avoid migration failures on older or malformed entries.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The lint/test GitHub workflow is configured to use Python `3.14.6`, which doesn’t currently exist; consider switching to a supported version (e.g., `3.12.x`) that matches Home Assistant’s supported runtime so CI can actually run.
- In `async_migrate_entry`, the call to `async_entity_id_to_device_id(hass, options[CONF_SOURCE_SENSOR])` assumes the `CONF_SOURCE_SENSOR` option is always present and non-null; you may want to guard this with a `get` or early return to avoid migration failures on older or malformed entries.
## Individual Comments
### Comment 1
<location path="custom_components/utility_meter_next_gen/config_flow.py" line_range="287-289" />
<code_context>
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize the options flow handler."""
- self.options_schema =None
+ self.options_schema = None
self.config_type = None
- self.data = config_entry.options.copy() #noqa: PGH003 # type: ignore
+ self.data = config_entry.options.copy() # noqa: PGH003 # type: ignore
_LOGGER.debug("async Option init self.data: %s", self.data)
if config_entry.options["config_type"] == CONF_CONFIG_CRON:
</code_context>
<issue_to_address>
**issue (bug_risk):** Store the config_entry on the OptionsFlowHandler instance to avoid AttributeError later.
`async_step_init` and `async_multi_option_step_2` access `self.config_entry.title`, but `config_entry` is only passed into `__init__` and never stored on `self`, so these calls will raise an `AttributeError`. Please store it (e.g. `self.config_entry = config_entry` in `__init__`) and use that attribute in the later methods.
</issue_to_address>
### Comment 2
<location path="custom_components/utility_meter_next_gen/sensor.py" line_range="148-149" />
<code_context>
unique_id = config_entry.entry_id
- device_info = async_device_info_to_link_from_entity(
+ device = async_entity_id_to_device(
hass,
config_entry.options[CONF_SOURCE_SENSOR],
)
</code_context>
<issue_to_address>
**issue (bug_risk):** Using DeviceEntry directly does not attach the sensor to a device; map it to a DeviceInfo and expose it via device_info.
`async_device_info_to_link_from_entity` previously set `_attr_device_info`, which HA uses to link entities to devices. After switching to `async_entity_id_to_device`, you store the `DeviceEntry` on `self.device_entry`, but HA doesn’t use that attribute, so these sensors may no longer be linked to any device in the UI/registry.
Please either keep `_attr_device_info` populated (e.g. by building a `DeviceInfo` from the `DeviceEntry`) or expose it via a `device_info` property that constructs and returns a `DeviceInfo`. The same issue exists in `UtilityMeterSensor.__init__`, where `device_info` was replaced with `device: DeviceEntry | None` and stored only as `self.device_entry`.
</issue_to_address>
### Comment 3
<location path="custom_components/utility_meter_next_gen/select.py" line_range="34-36" />
<code_context>
unique_id = config_entry.entry_id
- device_info = async_device_info_to_link_from_entity(
+ device = async_entity_id_to_device(
hass,
config_entry.options[CONF_SOURCE_SENSOR],
)
</code_context>
<issue_to_address>
**issue (bug_risk):** Tariff select entity is no longer linked to a device after switching to DeviceEntry.
Previously the select set `_attr_device_info` from `async_device_info_to_link_from_entity`, so HA could link it to the source device. Now it only stores a `DeviceEntry` on `self.device_entry` and never exposes a `DeviceInfo`, so the UI device association will be lost. Please mirror the sensor approach by either keeping `_attr_device_info` set or adding a `device_info` property that derives a `DeviceInfo` from the `DeviceEntry`.
</issue_to_address>
### Comment 4
<location path=".github/workflows/lint.yml" line_range="46" />
<code_context>
+ - name: Set up Python
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ with:
+ python-version: "3.14.6"
+ cache: "pip"
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Configured Python version 3.14.6 is invalid and will break CI.
`python-version: "3.14.6"` is not a valid release (current stable is 3.12), so this workflow will fail before tests run. Please pin to a supported version (e.g., 3.11 or 3.12) that aligns with Home Assistant and pytest-homeassistant-custom-component requirements.
</issue_to_address>
### Comment 5
<location path="tests/components/utility_meter_evolved/test_select.py" line_range="11-8" />
<code_context>
+from pytest_homeassistant_custom_component.common import MockConfigEntry
async def test_select_entity_name_config_entry(
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test to verify that the select entity filters out the 'total' tariff case-insensitively
The current changes rely on `TariffSelect.options` filtering out the "total" tariff via a case-insensitive regex, but there’s no test explicitly covering this (including mixed-case values like "Total"/"TOTAL").
Please add a test similar to the example below to ensure case-insensitive filtering of "total" is enforced and to guard against regressions:
```python
async def test_select_options_filters_total_case_insensitive(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
) -> None:
source_entry = MockConfigEntry(domain="test")
source_entry.add_to_hass(hass)
source_device = device_registry.async_get_or_create(
config_entry_id=source_entry.entry_id,
identifiers={("test", "source")},
)
source_entity = entity_registry.async_get_or_create(
"sensor", "test", "source",
config_entry=source_entry,
device_id=source_device.id,
)
config_entry = MockConfigEntry(
domain=DOMAIN,
options={
CONF_NAME: "Energy",
CONF_TARIFFS: ["Peak", "Total", "OFFPEAK"],
CONF_SOURCE_SENSOR: source_entity.entity_id,
},
)
config_entry.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("select.energy")
assert state is not None
assert set(state.attributes["options"]) == {"Peak", "OFFPEAK"}
```
Suggested implementation:
```python
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from pytest_homeassistant_custom_component.common import MockConfigEntry
async def test_select_options_filters_total_case_insensitive(
hass,
entity_registry,
device_registry,
) -> None:
source_entry = MockConfigEntry(domain="test")
source_entry.add_to_hass(hass)
source_device = device_registry.async_get_or_create(
config_entry_id=source_entry.entry_id,
identifiers={("test", "source")},
)
source_entity = entity_registry.async_get_or_create(
"sensor",
"test",
"source",
config_entry=source_entry,
device_id=source_device.id,
)
config_entry = MockConfigEntry(
domain=DOMAIN,
options={
CONF_NAME: "Energy",
CONF_TARIFFS: ["Peak", "Total", "OFFPEAK"],
CONF_SOURCE_SENSOR: source_entity.entity_id,
},
)
config_entry.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("select.energy")
assert state is not None
assert set(state.attributes["options"]) == {"Peak", "OFFPEAK"}
async def test_select_entity_name_config_entry(
```
This new test relies on the constants `DOMAIN`, `CONF_NAME`, `CONF_TARIFFS`, and `CONF_SOURCE_SENSOR` being imported elsewhere in the file (as is typical for this test module). If they are not already imported, you should add the appropriate imports at the top of the file, e.g.:
- `from homeassistant.components.utility_meter_evolved.const import DOMAIN`
- `from homeassistant.const import CONF_NAME`
- `from custom_components.utility_meter_evolved.const import CONF_TARIFFS, CONF_SOURCE_SENSOR`
Adjust the import paths to match how they are defined in your integration.
</issue_to_address>
### Comment 6
<location path="README.md" line_range="32" />
<code_context>
+## Compatibility
+
+Release 2026.8.0 and newer require Home Assistant 2026.8.0 or later. Utility
+Meter Next Gen entities link directly to their source device while the source
+integration remains the device's sole owner, matching Home Assistant's current
</code_context>
<issue_to_address>
**issue (typo):** Fix subject–verb agreement in the compatibility sentence.
Consider either "Release 2026.8.0 and newer requires Home Assistant 2026.8.0 or later" or "Releases 2026.8.0 and newer require Home Assistant 2026.8.0 or later" to match the singular/plural subject with the verb.
```suggestion
Releases 2026.8.0 and newer require Home Assistant 2026.8.0 or later. Utility
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| self.options_schema = None | ||
| self.config_type = None | ||
| self.data = config_entry.options.copy() #noqa: PGH003 # type: ignore | ||
| self.data = config_entry.options.copy() # noqa: PGH003 # type: ignore |
There was a problem hiding this comment.
issue (bug_risk): Store the config_entry on the OptionsFlowHandler instance to avoid AttributeError later.
async_step_init and async_multi_option_step_2 access self.config_entry.title, but config_entry is only passed into __init__ and never stored on self, so these calls will raise an AttributeError. Please store it (e.g. self.config_entry = config_entry in __init__) and use that attribute in the later methods.
| device = async_entity_id_to_device( | ||
| hass, |
There was a problem hiding this comment.
issue (bug_risk): Using DeviceEntry directly does not attach the sensor to a device; map it to a DeviceInfo and expose it via device_info.
async_device_info_to_link_from_entity previously set _attr_device_info, which HA uses to link entities to devices. After switching to async_entity_id_to_device, you store the DeviceEntry on self.device_entry, but HA doesn’t use that attribute, so these sensors may no longer be linked to any device in the UI/registry.
Please either keep _attr_device_info populated (e.g. by building a DeviceInfo from the DeviceEntry) or expose it via a device_info property that constructs and returns a DeviceInfo. The same issue exists in UtilityMeterSensor.__init__, where device_info was replaced with device: DeviceEntry | None and stored only as self.device_entry.
| device = async_entity_id_to_device( | ||
| hass, | ||
| config_entry.options[CONF_SOURCE_SENSOR], |
There was a problem hiding this comment.
issue (bug_risk): Tariff select entity is no longer linked to a device after switching to DeviceEntry.
Previously the select set _attr_device_info from async_device_info_to_link_from_entity, so HA could link it to the source device. Now it only stores a DeviceEntry on self.device_entry and never exposes a DeviceInfo, so the UI device association will be lost. Please mirror the sensor approach by either keeping _attr_device_info set or adding a device_info property that derives a DeviceInfo from the DeviceEntry.
| - name: Set up Python | ||
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | ||
| with: | ||
| python-version: "3.14.6" |
There was a problem hiding this comment.
issue (bug_risk): Configured Python version 3.14.6 is invalid and will break CI.
python-version: "3.14.6" is not a valid release (current stable is 3.12), so this workflow will fail before tests run. Please pin to a supported version (e.g., 3.11 or 3.12) that aligns with Home Assistant and pytest-homeassistant-custom-component requirements.
| from homeassistant.setup import async_setup_component | ||
|
|
||
| from tests.common import MockConfigEntry | ||
| from pytest_homeassistant_custom_component.common import MockConfigEntry |
There was a problem hiding this comment.
suggestion (testing): Add a test to verify that the select entity filters out the 'total' tariff case-insensitively
The current changes rely on TariffSelect.options filtering out the "total" tariff via a case-insensitive regex, but there’s no test explicitly covering this (including mixed-case values like "Total"/"TOTAL").
Please add a test similar to the example below to ensure case-insensitive filtering of "total" is enforced and to guard against regressions:
async def test_select_options_filters_total_case_insensitive(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
) -> None:
source_entry = MockConfigEntry(domain="test")
source_entry.add_to_hass(hass)
source_device = device_registry.async_get_or_create(
config_entry_id=source_entry.entry_id,
identifiers={("test", "source")},
)
source_entity = entity_registry.async_get_or_create(
"sensor", "test", "source",
config_entry=source_entry,
device_id=source_device.id,
)
config_entry = MockConfigEntry(
domain=DOMAIN,
options={
CONF_NAME: "Energy",
CONF_TARIFFS: ["Peak", "Total", "OFFPEAK"],
CONF_SOURCE_SENSOR: source_entity.entity_id,
},
)
config_entry.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("select.energy")
assert state is not None
assert set(state.attributes["options"]) == {"Peak", "OFFPEAK"}Suggested implementation:
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from pytest_homeassistant_custom_component.common import MockConfigEntry
async def test_select_options_filters_total_case_insensitive(
hass,
entity_registry,
device_registry,
) -> None:
source_entry = MockConfigEntry(domain="test")
source_entry.add_to_hass(hass)
source_device = device_registry.async_get_or_create(
config_entry_id=source_entry.entry_id,
identifiers={("test", "source")},
)
source_entity = entity_registry.async_get_or_create(
"sensor",
"test",
"source",
config_entry=source_entry,
device_id=source_device.id,
)
config_entry = MockConfigEntry(
domain=DOMAIN,
options={
CONF_NAME: "Energy",
CONF_TARIFFS: ["Peak", "Total", "OFFPEAK"],
CONF_SOURCE_SENSOR: source_entity.entity_id,
},
)
config_entry.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("select.energy")
assert state is not None
assert set(state.attributes["options"]) == {"Peak", "OFFPEAK"}
async def test_select_entity_name_config_entry(This new test relies on the constants DOMAIN, CONF_NAME, CONF_TARIFFS, and CONF_SOURCE_SENSOR being imported elsewhere in the file (as is typical for this test module). If they are not already imported, you should add the appropriate imports at the top of the file, e.g.:
from homeassistant.components.utility_meter_evolved.const import DOMAINfrom homeassistant.const import CONF_NAMEfrom custom_components.utility_meter_evolved.const import CONF_TARIFFS, CONF_SOURCE_SENSOR
Adjust the import paths to match how they are defined in your integration.
|
|
||
| ## Compatibility | ||
|
|
||
| Release 2026.8.0 and newer require Home Assistant 2026.8.0 or later. Utility |
There was a problem hiding this comment.
issue (typo): Fix subject–verb agreement in the compatibility sentence.
Consider either "Release 2026.8.0 and newer requires Home Assistant 2026.8.0 or later" or "Releases 2026.8.0 and newer require Home Assistant 2026.8.0 or later" to match the singular/plural subject with the verb.
| Release 2026.8.0 and newer require Home Assistant 2026.8.0 or later. Utility | |
| Releases 2026.8.0 and newer require Home Assistant 2026.8.0 or later. Utility |
Summary
DeviceInfolinking with direct entity-to-source-device linksWhy
Home Assistant 2026.8 changes devices to have a single config-entry owner. The previous helper APIs are deprecated and no longer provide the ownership/linking behavior this integration relied on.
Validation
Reviewer notes
The original request referenced Home Assistant 2028.8.0, but that release/tag does not exist. This PR targets the released 2026.8.0 version containing the relevant device-management changes.
Summary by Sourcery
Adopt Home Assistant 2026.8’s single-owner device model by linking helper entities directly to their source devices, updating migrations, and tightening config/flow handling.
New Features:
Bug Fixes:
Enhancements:
Build:
Documentation: