Remove the YAML import path; code now matches the UI-only README

Deletes the YAML CONFIG_SCHEMA, async_setup import, async_step_import, the
yaml_managed options abort, and the hass.config.entry_updated listener that
was registered on every entry setup and never removed. A leftover YAML block
now gets HA's standard config-entry-only warning. OpenSpec change
remove-yaml-import archived (options-flow R7 removed).
This commit is contained in:
Casey 2026-09-25 19:10:51 +02:00
commit 675221941a
10 changed files with 47 additions and 135 deletions

View file

@ -1,21 +1,16 @@
"""Adaptive Lighting integration in Home Assistant (CDiT fork)."""
import logging
from typing import Any
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_SOURCE
from homeassistant.core import Event, HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers import entity_registry as er
from .const import (
_DOMAIN_SCHEMA, # pyright: ignore[reportPrivateUsage]
ATTR_ADAPTIVE_LIGHTING_MANAGER,
CONF_LUX_SENSOR,
CONF_NAME,
CONFIG_ENTRY_VERSION,
DOMAIN,
)
@ -31,41 +26,9 @@ PLATFORMS = ["switch", "number", "sensor"]
_REMOVED_UNIQUE_ID_SUFFIXES = ("_sleep_mode",)
def _all_unique_names(value: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Validate that all entities have a unique profile name."""
hosts = [device[CONF_NAME] for device in value]
schema = vol.Schema(vol.Unique())
schema(hosts)
return value
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_names)},
extra=vol.ALLOW_EXTRA,
)
async def reload_configuration_yaml(event: Event) -> None:
"""Reload configuration.yaml."""
hass: HomeAssistant | None = event.data.get("hass")
if hass is not None:
await hass.services.async_call("homeassistant", "check_config", {})
else:
_LOGGER.error("HomeAssistant instance not found in event data.")
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Import integration from config."""
if DOMAIN in config:
for entry in config[DOMAIN]:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_IMPORT},
data=entry,
),
)
return True
# UI-only: an `adaptive_lighting:` block in configuration.yaml is not imported.
# HA logs that the integration is set up from the UI instead.
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
def _remove_orphan_sleep_entities(
@ -155,10 +118,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
data = hass.data.setdefault(DOMAIN, {})
# Reload YAML configs on `hass.config.entry_updated` (covers `quick reload`
# and explicit `hass.reload_config_entry` calls).
hass.bus.async_listen("hass.config.entry_updated", reload_configuration_yaml)
data[config_entry.entry_id] = {
# Cache slot the master switch publishes to after each curve tick
# (add-output-sensors / D2). Initialized to None so any sensor that

View file

@ -12,7 +12,6 @@ from typing import Any
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_IMPORT,
ConfigEntry,
)
from homeassistant.config_entries import ConfigFlow as HAConfigFlow
@ -405,23 +404,6 @@ class AdaptiveLightingConfigFlow(HAConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_import(self, user_input: dict[str, Any] | None = None):
"""Handle a YAML import.
YAML-configured entries can be loaded into HA but cannot be edited
via the options flow — see `OptionsFlowHandler.async_step_init`.
"""
if user_input is None:
return self.async_abort(reason="no_data")
await self.async_set_unique_id(user_input[CONF_NAME])
data = self.hass.data.setdefault(DOMAIN, {})
data.setdefault("__yaml__", set()).add(self.unique_id)
for entry in self._async_current_entries():
if entry.unique_id == self.unique_id:
self.hass.config_entries.async_update_entry(entry, data=user_input)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
@staticmethod
@callback
def async_get_options_flow(
@ -440,8 +422,7 @@ class OptionsFlowHandler(OptionsFlowWithReload):
"""Sectioned options flow with reload-on-save.
Spec R6: extends OptionsFlowWithReload so saving triggers a clean
reload of the entry. Spec R7: YAML-managed entries abort with a
translation-keyed message instead of presenting an editable form.
reload of the entry.
"""
def _overlay_range_values(self, current: dict[str, Any]) -> None:
@ -467,9 +448,6 @@ class OptionsFlowHandler(OptionsFlowWithReload):
async def async_step_init(self, user_input: dict[str, Any] | None = None):
"""Render the single-page options form and handle its submission."""
conf = self.config_entry
if conf.source == SOURCE_IMPORT:
return self.async_abort(reason="yaml_managed")
# Merge data and options to compute the effective "current" view.
current = dict(conf.data)
current.update(conf.options)

View file

@ -315,8 +315,8 @@ def int_between(min_int: int, max_int: int) -> vol.All:
return vol.All(vol.Coerce(int), vol.Range(min=min_int, max=max_int))
# Per-field validation tuples driving YAML import and (via maybe_coerce)
# the YAML schema. The UI options flow does not consume this list directly;
# Per-field validation tuples: switch.py validates and defaults entry options
# against them. The UI options flow does not consume this list directly;
# config_flow.py hand-shapes its sections via HA selectors. Eighteen entries:
# 16 retained from upstream + 2 new sun-entity fields.
VALIDATION_TUPLES: list[tuple[str, Any, Any]] = [
@ -348,35 +348,14 @@ def timedelta_as_int(value: timedelta) -> float:
return value.total_seconds()
# Only CONF_INTERVAL still needs YAML-time coercion (HA accepts strings like
# "00:01:30" and turns them into timedeltas). All other coerced fields
# (sun times, brightness-mode windows) were removed.
# Only CONF_INTERVAL still needs coercion (HA accepts strings like "00:01:30"
# and turns them into timedeltas). All other coerced fields (sun times,
# brightness-mode windows) were removed.
EXTRA_VALIDATION: dict[str, tuple[Any, Any]] = {
CONF_INTERVAL: (cv.time_period, timedelta_as_int),
}
def maybe_coerce(key: str, validation: Any) -> vol.All | Any:
"""Coerce the validation into a json serializable type."""
validation, coerce = EXTRA_VALIDATION.get(key, (validation, None))
if coerce is not None:
return vol.All(validation, vol.Coerce(coerce))
return validation
_yaml_validation_tuples = [
(key, default, maybe_coerce(key, validation))
for key, default, validation in VALIDATION_TUPLES
] + [(CONF_NAME, DEFAULT_NAME, cv.string)]
_DOMAIN_SCHEMA = vol.Schema(
{
vol.Optional(key, default=default): validation
for key, default, validation in _yaml_validation_tuples
},
)
def apply_service_schema(initial_transition: int = 1) -> vol.Schema:
"""Return the schema for the apply service."""
return vol.Schema(

View file

@ -13,8 +13,7 @@
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_data": "Cannot import without configuration data."
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"incompatible_version": "This entry was created with an older, incompatible version of Adaptive Lighting. Delete the entry and create a new one."
@ -125,9 +124,6 @@
}
}
},
"abort": {
"yaml_managed": "This Adaptive Lighting profile is managed by YAML. To change its options, edit configuration.yaml."
},
"error": {
"option_error": "One of the options has an invalid value.",
"entity_missing": "One or more of the selected light entities are not currently registered in Home Assistant."

View file

@ -13,8 +13,7 @@
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_data": "Cannot import without configuration data."
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"incompatible_version": "This entry was created with an older, incompatible version of Adaptive Lighting. Delete the entry and create a new one."
@ -113,9 +112,6 @@
}
}
},
"abort": {
"yaml_managed": "This Adaptive Lighting profile is managed by YAML. To change its options, edit configuration.yaml."
},
"error": {
"option_error": "One of the options has an invalid value.",
"entity_missing": "One or more of the selected light entities are not currently registered in Home Assistant."

View file

@ -0,0 +1,19 @@
## Why
The README says setup is UI-only and that `adaptive_lighting:` blocks in `configuration.yaml` are ignored, but the code still carried the upstream YAML import path: a full `CONFIG_SCHEMA`, an `async_setup` that imported each YAML block as a config entry, `async_step_import`, a `yaml_managed` abort in the options flow, and a `hass.config.entry_updated` listener (registered on every entry setup and never removed) that ran `homeassistant.check_config`. Code and docs disagreed, and the listener leaked.
## What Changes
- Delete the YAML import path: the YAML `CONFIG_SCHEMA` and its helpers, `async_setup`, `reload_configuration_yaml` and its bus listener, `async_step_import`, the `yaml_managed` options-flow abort and the `no_data` / `yaml_managed` strings.
- `CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)`: a YAML block now gets HA's standard "set up from the UI" warning instead of a silent import.
- Remove the options-flow requirement that YAML-managed entries abort.
## Capabilities
### Modified Capabilities
- `options-flow`: the YAML-managed abort requirement is removed.
## Impact
`__init__.py`, `config_flow.py`, `const.py`, `strings.json`, `translations/en.json`, `tests/test_config_flow.py`. No config-entry version change: UI entries are untouched.

View file

@ -0,0 +1,6 @@
## REMOVED Requirements
### Requirement: YAML-managed config entries cannot be edited via the options dialog
**Reason**: The fork no longer imports YAML at all, so no entry can be YAML-managed.
**Migration**: None. Configure profiles in the UI; a leftover `adaptive_lighting:` block only produces HA's standard UI-only warning.

View file

@ -0,0 +1,7 @@
## 1. Remove the YAML import path
- [x] 1.1 Delete YAML `CONFIG_SCHEMA`, `_all_unique_names`, `async_setup`, `reload_configuration_yaml` and the `hass.config.entry_updated` listener; add `cv.config_entry_only_config_schema`
- [x] 1.2 Delete `async_step_import` and the `yaml_managed` abort in the options flow
- [x] 1.3 Delete `_DOMAIN_SCHEMA`, `_yaml_validation_tuples`, `maybe_coerce`
- [x] 1.4 Drop `no_data` / `yaml_managed` strings and the yaml_managed test
- [x] 1.5 Tests, ruff and CI (hassfest, HACS, pytest) green

View file

@ -2,7 +2,9 @@
## Purpose
TBD - created by archiving change cdit-config-redesign. Update Purpose after archive.
## Requirements
### Requirement: Options dialog presents fields in named collapsible sections
The integration options dialog SHALL group its configurable fields into seven named sections, rendered using Home Assistant's `section()` schema helper. Section names and field membership SHALL match the layout below.
@ -215,17 +217,6 @@ The options flow class SHALL extend `homeassistant.config_entries.OptionsFlowWit
- **THEN** the entity IDs of the AL device's switches SHALL remain unchanged
- **AND** no duplicate entities SHALL appear in the entity registry
### Requirement: YAML-managed config entries cannot be edited via the options dialog
When a config entry was created from `configuration.yaml` rather than the UI, the options flow SHALL abort with `async_abort(reason="yaml_managed")` instead of presenting an editable form. The abort SHALL produce a translation-keyed message in the HA UI that directs the user to edit `configuration.yaml`.
#### Scenario: Opening options on a YAML-managed entry shows an abort message
- **WHEN** the user navigates to Configure on a config entry whose `source == SOURCE_IMPORT`
- **THEN** the options flow SHALL abort with reason `yaml_managed`
- **AND** the HA UI SHALL display a message indicating the entry is YAML-managed and must be edited in `configuration.yaml`
- **AND** no editable form SHALL be shown
### Requirement: Incompatible config entry versions fail to load with a clear error
The integration's `manifest.json` SHALL declare a major `version` that increments on every breaking config-schema change. On `async_setup_entry`, the integration SHALL reject any config entry whose stored `version` is older than the current major and SHALL raise `ConfigEntryError` with a user-facing message instructing the user to recreate the entry. No silent migration of dropped fields SHALL occur.

View file

@ -5,7 +5,6 @@ Covers spec/options-flow/spec.md requirements R1, R2, R3, R5, R6, R7.
from __future__ import annotations
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_NAME
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.selector import (
@ -337,24 +336,6 @@ async def test_user_flow_creates_entry(hass) -> None:
assert result["title"] == "living room"
async def test_yaml_managed_entry_aborts_options_flow(hass) -> None:
"""R7: options flow on a SOURCE_IMPORT entry aborts with yaml_managed."""
entry = MockConfigEntry(
domain=DOMAIN,
title=DEFAULT_NAME,
data={CONF_NAME: DEFAULT_NAME},
options={},
source=SOURCE_IMPORT,
version=2,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "yaml_managed"
async def test_options_flow_renders_sectioned_schema(hass) -> None:
"""R1 + R6: opening options on a UI-managed entry shows the sectioned form."""
entry = MockConfigEntry(