mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-16 08:44:03 +02:00
Test infrastructure: - Add `pytest-homeassistant-custom-component` (PHACC) as a `test` dependency group. PHACC ships `hass`, `enable_custom_integrations`, `MockConfigEntry`, and friends without needing to clone HA core as a sibling directory. Modernizes the test setup from upstream's `setup-symlinks` pattern. - conftest.py: add `auto_enable_custom_integrations` autouse fixture so HA discovers the integration under `custom_components/` during tests. Keep the upstream template-deprecation no-op. - Import paths: all tests now import from `custom_components.adaptive_lighting` (not `homeassistant.components.adaptive_lighting`) and from `pytest_homeassistant_custom_component.common` (not `tests.common`). New tests (groups 7.2-7.16): - tests/test_color_and_brightness.py (16 tests): TestBrightnessCurve asserts min-before-sunrise, midpoint-at-event, max-during-day, sunset ramp symmetry. TestColorTempCurve verifies the same shape applies to K. TestSunPosition checks the synthetic +1/-1/0 derivation. TestTanhDayCurveDirect exercises the helper directly. - tests/test_config_flow.py (12 tests): six sections in order, each section contains only its specified fields, conditional visibility of send_split_delay, default sun entities, strict-typed entity selectors, NumberSelector slider/box configs, BooleanSelector for every flag, full user→create-entry flow, YAML-managed entry aborts with `yaml_managed` reason, options flow renders the sectioned schema. - tests/test_init.py (6 tests): successful setup on current version, stale version raises ConfigEntryError via async_migrate_entry, unload is clean, tombstone removes orphan sleep entity + logs INFO, tombstone is idempotent, tombstone respects config_entry ownership. Source modernizations driven by the tests: - __init__.py: add `async_migrate_entry` that surfaces the "incompatible — delete and recreate" message and sets the entry to MIGRATION_ERROR. Removed direct `ConfigEntryError` from `async_setup_entry` (HA routes version mismatches through the migration handler now). - switch.py: removed upstream's YAML-managed-entry auto-remove hack. YAML profiles now load normally and the options flow handles the "you must edit configuration.yaml" message (spec R7). Existing tests updated to PHACC paths: test_adaptation_utils.py (38 tests), test_hass_utils.py (22 tests). Both pass without modification beyond the import fix. Deleted: tests/test_switch.py (2,999 LOC of upstream tests, most covering sleep mode / take-over-control / manual-control state machines that no longer exist; CDiT-specific switch tests deferred to a follow-up change). Result: `uv run --group test pytest tests/` → 94 passed in 0.57s. Deferred from this change: - 4.3: manual UI test that toggling a field and saving reloads cleanly (requires a real HA instance, can't be done from CLI). - 9.4: GitHub repo description / topics update (do via `gh repo edit` outside the change scope). openspec status: 4/4 artifacts complete; strict-validate green.
159 lines
5.4 KiB
Python
159 lines
5.4 KiB
Python
"""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.exceptions import ConfigEntryError
|
|
from homeassistant.helpers import entity_registry as er
|
|
|
|
from .const import (
|
|
_DOMAIN_SCHEMA, # pyright: ignore[reportPrivateUsage]
|
|
ATTR_ADAPTIVE_LIGHTING_MANAGER,
|
|
CONF_NAME,
|
|
CONFIG_ENTRY_VERSION,
|
|
DOMAIN,
|
|
UNDO_UPDATE_LISTENER,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
PLATFORMS = ["switch"]
|
|
|
|
# unique_id suffix(es) that this fork no longer creates. Any entity in the
|
|
# registry whose unique_id ends with one of these strings AND that is owned
|
|
# by an Adaptive Lighting config entry is a leftover from upstream and is
|
|
# removed on first setup (spec R9, design D12).
|
|
_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
|
|
|
|
|
|
def _remove_orphan_sleep_entities(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
) -> None:
|
|
"""Remove sleep-mode switch entities left behind by upstream AL.
|
|
|
|
Idempotent: subsequent runs find nothing and emit no log lines. Only
|
|
removes entities whose config_entry_id matches the current entry, so
|
|
foreign entities matching the name pattern are not touched.
|
|
Spec R9, design D12.
|
|
"""
|
|
registry = er.async_get(hass)
|
|
entries_to_remove = [
|
|
entry.entity_id
|
|
for entry in registry.entities.values()
|
|
if entry.config_entry_id == config_entry.entry_id
|
|
and any(
|
|
entry.unique_id.endswith(suffix)
|
|
for suffix in _REMOVED_UNIQUE_ID_SUFFIXES
|
|
)
|
|
]
|
|
for entity_id in entries_to_remove:
|
|
_LOGGER.info(
|
|
"Removing orphan entity %s left behind by upstream Adaptive Lighting "
|
|
"(sleep mode is not supported in the CDiT fork).",
|
|
entity_id,
|
|
)
|
|
registry.async_remove(entity_id)
|
|
|
|
|
|
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
|
"""Reject older config-entry versions with a friendly recreate message.
|
|
|
|
Spec R8 + design D4: this fork deliberately does not migrate upstream
|
|
entries. Define a migration handler so HA routes version mismatches
|
|
here (instead of logging the generic "Migration handler not found")
|
|
and surface a `ConfigEntryError` whose message tells the user what to
|
|
do.
|
|
"""
|
|
msg = (
|
|
f"Adaptive Lighting v{CONFIG_ENTRY_VERSION} (CDiT fork) is incompatible "
|
|
f"with the existing config entry (version {config_entry.version}). "
|
|
"Delete the entry and recreate it from Settings → Devices & Services."
|
|
)
|
|
raise ConfigEntryError(msg)
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
|
"""Set up the component."""
|
|
_remove_orphan_sleep_entities(hass, config_entry)
|
|
|
|
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)
|
|
|
|
undo_listener = config_entry.add_update_listener(async_update_options)
|
|
data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener}
|
|
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
|
|
|
return True
|
|
|
|
|
|
async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
|
"""Update options."""
|
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
|
"""Unload a config entry."""
|
|
unload_ok = await hass.config_entries.async_forward_entry_unload(
|
|
config_entry,
|
|
"switch",
|
|
)
|
|
data = hass.data[DOMAIN]
|
|
data[config_entry.entry_id][UNDO_UPDATE_LISTENER]()
|
|
if unload_ok:
|
|
data.pop(config_entry.entry_id)
|
|
|
|
if len(data) == 1 and ATTR_ADAPTIVE_LIGHTING_MANAGER in data:
|
|
# no more config_entries
|
|
manager = data.pop(ATTR_ADAPTIVE_LIGHTING_MANAGER)
|
|
manager.disable()
|
|
|
|
if not data:
|
|
hass.data.pop(DOMAIN)
|
|
|
|
return unload_ok
|