mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-10-29 14:37:47 +01:00
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:
parent
c066c1ba99
commit
675221941a
10 changed files with 47 additions and 135 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue