mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-17 17:24:03 +02:00
const.py:
- Drop 21 retired CONF_*/DEFAULT_* (sleep cluster, manual sun timing,
brightness curve variants, take-over-control cluster).
- Add CONF_SUNRISE_ENTITY / CONF_SUNSET_ENTITY (default
sensor.sun_next_rising / _setting).
- Add RAMP_HALF_WIDTH_SECONDS = 1800.
- Add CONFIG_ENTRY_VERSION = 2 (gate for strict version-break).
- Re-default DEFAULT_MIN_BRIGHTNESS 1→5, DEFAULT_MIN_COLOR_TEMP 2000→2200.
- Update ICON_MAIN / _BRIGHTNESS / _COLOR_TEMP to the CDiT picks.
manifest.json: bump to 2.0.0-cdit.1, pin homeassistant: 2025.1.0,
re-point codeowners/documentation/issue_tracker to the CaseyRo fork.
switch.py:
- Drop sleep_mode_switch creation in async_setup_entry; integration
now creates 3 switches per profile (master, adapt_color, adapt_brightness).
- Strip sleep-mode state machine from AdaptiveSwitch (sleep_mode_switch
attribute, sleep_transition, adapt_until_sleep, the state-change
listener, the _sleep_mode_switch_state_event_action method).
- Bridge-stub _take_over_control / _detect_non_ha_changes /
_adapt_only_on_bare_turn_on / _only_once / _auto_reset_manual_control_time
as class-level False/0 so Manager-side branches become dead code without
needing a full rewrite of AdaptiveLightingManager.
- Remove handle_set_manual_control service + its registration.
- Replace astral-driven curve init with the new SunLightSettings call
shape; add AdaptiveSwitch._today_sun_events() helper that reads the
configured sunrise/sunset entities.
- Set _attr_icon on AdaptiveSwitch.
color_and_brightness.py:
- Rewrite SunLightSettings as a pure curve-math wrapper (5 fields:
name + bounds + ramp half-width); methods take t_sunrise/t_sunset
as args, never reads HA state.
- Implement piecewise tanh ramp curve per spec R4 / design D11 in
_tanh_day_curve(). Both brightness and color-temp use the same shape.
- Drop SunEvents, sleep-mode branches, brightness_mode-switch, astral
dependency, force_rgb_color, lerp_color_hsv sleep-tinted color blending.
config_flow.py:
- Rewrite as sectioned schema (Targets / Daytime curve / Sun schedule /
Light control / Advanced / Diagnostics) via HA's section() helper.
- Native HA selectors throughout (NumberSelector, EntitySelector,
BooleanSelector). Strict typing on sun-event entity pickers.
- Conditional visibility for send_split_delay (driver:
separate_turn_on_commands).
- Extend OptionsFlowWithReload (with ImportError fallback for HA < 2025.1
dev environment); async_abort(reason="yaml_managed") for SOURCE_IMPORT.
- VERSION = CONFIG_ENTRY_VERSION.
__init__.py:
- Reject older config entries with ConfigEntryError ("recreate the entry")
per spec R8 / design D4.
- Add _remove_orphan_sleep_entities tombstone helper that scans the
entity registry for sleep-mode entities owned by this entry and
removes them on first setup. Idempotent and config_entry-scoped.
_docs_helpers.py: stub the removed DOCS_MANUAL_CONTROL / SET_MANUAL_CONTROL_SCHEMA
so the docs-generator script keeps importing.
Validation:
- ALL modules import cleanly under HA 2024.12.5 (dev env).
- openspec validate cdit-config-redesign --strict: green.
- ruff check --select=F: clean.
Remaining: groups 7 (16 tests), 8 (strings + plain-language pass), 9 (docs).
122 lines
3.9 KiB
Python
122 lines
3.9 KiB
Python
from typing import Any
|
|
|
|
import homeassistant.helpers.config_validation as cv
|
|
import pandas as pd
|
|
import voluptuous as vol
|
|
from homeassistant.helpers import selector
|
|
|
|
from .const import (
|
|
DOCS,
|
|
DOCS_APPLY,
|
|
VALIDATION_TUPLES,
|
|
apply_service_schema,
|
|
)
|
|
|
|
# Stubs for upstream docs sections that no longer exist in the CDiT fork.
|
|
# The docs generator script reads these by name; provide empty placeholders
|
|
# so the script does not crash. The section in the rendered README will be
|
|
# empty and can be deleted from the template separately.
|
|
DOCS_MANUAL_CONTROL: dict[str, str] = {}
|
|
SET_MANUAL_CONTROL_SCHEMA = None
|
|
|
|
|
|
def _format_voluptuous_instance(instance: vol.All) -> str:
|
|
coerce_type = None
|
|
min_val = None
|
|
max_val = None
|
|
|
|
for validator in instance.validators:
|
|
if isinstance(validator, vol.Coerce):
|
|
coerce_type = validator.type.__name__
|
|
elif isinstance(validator, vol.Clamp | vol.Range):
|
|
min_val = validator.min
|
|
max_val = validator.max
|
|
|
|
if min_val is not None and max_val is not None:
|
|
return f"`{coerce_type}` {min_val}-{max_val}"
|
|
if min_val is not None:
|
|
return f"`{coerce_type} > {min_val}`"
|
|
if max_val is not None:
|
|
return f"`{coerce_type} < {max_val}`"
|
|
return f"`{coerce_type}`"
|
|
|
|
|
|
def _type_to_str(type_: Any) -> str: # noqa: PLR0911
|
|
"""Convert a (voluptuous) type to a string."""
|
|
if type_ == cv.entity_ids:
|
|
return "list of `entity_id`s"
|
|
if type_ in (bool, int, float, str):
|
|
return f"`{type_.__name__}`"
|
|
if type_ == cv.boolean:
|
|
return "bool"
|
|
if isinstance(type_, vol.All):
|
|
return _format_voluptuous_instance(type_)
|
|
if isinstance(type_, vol.Any):
|
|
return " or ".join(_type_to_str(t) for t in type_.validators)
|
|
if isinstance(type_, vol.In):
|
|
return f"one of `{type_.container}`"
|
|
if isinstance(type_, selector.SelectSelector):
|
|
return f"one of `{type_.config['options']}`"
|
|
if isinstance(type_, selector.ColorRGBSelector):
|
|
return "RGB color"
|
|
msg = f"Unknown type: {type_}"
|
|
raise ValueError(msg)
|
|
|
|
|
|
def generate_config_markdown_table() -> str:
|
|
rows: list[dict[str, str]] = []
|
|
for k, default, type_ in VALIDATION_TUPLES:
|
|
description = DOCS[k]
|
|
row = {
|
|
"Variable name": f"`{k}`",
|
|
"Description": description,
|
|
"Default": f"`{default}`",
|
|
"Type": _type_to_str(type_),
|
|
}
|
|
rows.append(row)
|
|
|
|
df = pd.DataFrame(rows)
|
|
return df.to_markdown(index=False)
|
|
|
|
|
|
def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[Any, Any]]:
|
|
result: dict[str, tuple[Any, Any]] = {}
|
|
for key, value in schema.schema.items():
|
|
if isinstance(key, vol.Optional):
|
|
default_value = key.default
|
|
result[key.schema] = (default_value, value)
|
|
return result
|
|
|
|
|
|
def _generate_service_markdown_table(
|
|
schema: dict[str, tuple[Any, Any]] | vol.Schema,
|
|
alternative_docs: dict[str, str] | None = None,
|
|
) -> str:
|
|
schema_dict = _schema_to_dict(schema) if isinstance(schema, vol.Schema) else schema
|
|
rows: list[dict[str, str]] = []
|
|
for k, (default, type_) in schema_dict.items():
|
|
if alternative_docs is not None and k in alternative_docs:
|
|
description = alternative_docs[k]
|
|
else:
|
|
description = DOCS[k]
|
|
row = {
|
|
"Service data attribute": f"`{k}`",
|
|
"Description": description,
|
|
"Required": "✅" if default == vol.UNDEFINED else "❌",
|
|
"Type": _type_to_str(type_),
|
|
}
|
|
rows.append(row)
|
|
|
|
df = pd.DataFrame(rows)
|
|
return df.to_markdown(index=False)
|
|
|
|
|
|
def generate_apply_markdown_table() -> str:
|
|
return _generate_service_markdown_table(apply_service_schema(), DOCS_APPLY)
|
|
|
|
|
|
def generate_set_manual_control_markdown_table() -> str:
|
|
return _generate_service_markdown_table(
|
|
SET_MANUAL_CONTROL_SCHEMA,
|
|
DOCS_MANUAL_CONTROL,
|
|
)
|